数据结构学习总结(2) 线性表之单链表

     一,回忆链表

  链表,别名链式存储结构或单链表,用于存储逻辑关系为 "一对一" 的数据。与顺序表不同,链表不限制数据的物理存储状态,换句话说,使用链表存储的数据元素,其物理存储位置是随机的。

     例如,使用链表存储 {1,2,3},数据的物理存储状态如 1 所示:


                   链表随机存储数据
                        图 1 链表随机存储数据
  我们看到,图 1 根本无法体现出各数据之间的逻辑关系。对此,链表的解决方案是,每个数据元素在存储时都配备一个指针,用于指向自己的直接后继元素。如图 2 所示:

                      各数据元素配备指针
                        图 2 各数据元素配备指针

  像图 2 这样,数据元素随机存储,并通过指针表示数据之间逻辑关系的存储结构就是链式存储结构。

   链表的节点

     从图 2 可以看到,链表中每个数据的存储都由以下两部分组成:

  1. 数据元素本身,其所在的区域称为数据域;
  2. 指向直接后继元素的指针,所在的区域称为指针域;

  即链表中存储各数据元素的结构如图 3 所示:

                       
                          图 3 节点结构

    图 3 所示的结构在链表中称为节点。也就是说,链表实际存储的是一个一个的节点,真正的数据元素包含在这些节点中,如图 4 所示:

                   链表中的节点
                          图 4 链表中的节点
   因此,链表中每个节点的具体实现,需要使用 C 语言中的结构体,具体实现代码为:
typedef struct Link{
    char elem; //代表数据域
    struct Link * next; //代表指针域,指向直接后继元素
}link; //link为节点名,每个节点都是一个 link 结构体

  提示,由于指针域中的指针要指向的也是一个节点,因此要声明为 Link 类型(这里要写成 struct Link* 的形式)。

 头节点,头指针,和首元节点

      其实,图 4 所示的链表结构并不完整。一个完整的链表需要由以下几部分构成:

  1. 头指针:一个普通的指针,它的特点是永远指向链表第一个节点的位置。很明显,头指针用于指明链表的位置,便于后期找到链表并使用表中的数据;
  2. 节点:链表中的节点又细分为头节点、首元节点和其他节点:
    • 头节点:其实就是一个不存任何数据的空节点,通常作为链表的第一个节点。对于链表来说,头节点不是必须的,它的作用只是为了方便解决某些实际问题;
    • 首元节点:由于头节点(也就是空节点)的缘故,链表中称第一个存有数据的节点为首元节点。首元节点只是对链表中第一个存有数据节点的一个称谓,没有实际意义;
    • 其他节点:链表中其他的节点;

 注意:关于头指针P:分为指针p空间和指针p所指向的空间

  理解:前面定义指针变量p并分配所指向空间的语句(link*)malloc(sizeof(link));部分是由计算机分配一个没有名字的内存空间,通过将其首地址放到变量p中,程序员就可以用变量p间接的操作这块空间,为简化,通常不画出p变量的空间,故可以表示为如下:

       

因此,一个存储 {1,2,3} 的完整链表结构如图 5 所示: 

        完整的链表示意图

注意:链表中有头节点时,头指针指向头节点;反之,若链表中没有头节点,则头指针指向首元节点。

 明白了链表的基本结构,下面我们来学习如何创建一个链表。

 链表的创建(初始化)

         创建一个链表需要做如下工作:

  1. 声明一个头指针(如果有必要,可以声明一个头节点);
  2. 创建多个存储数据的节点,在创建的过程中,要随时与其前驱节点建立逻辑关系;

例如,创建一个存储 {1,2,3,4} 且无头节点的链表,C 语言实现代码如下:

link * initLink(){
    link * p=NULL;//创建头指针
    link * temp = (link*)malloc(sizeof(link));//创建首元节点
    //首元节点先初始化
    temp->elem = 1;
    temp->next = NULL;
    p = temp;//头指针指向首元节点
    //从第二个节点开始创建
    for (int i=2; i<5; i++) {
     //创建一个新节点并初始化
        link *a=(link*)malloc(sizeof(link));
        a->elem=i;
        a->next=NULL;
        //将temp节点与新建立的a节点建立逻辑关系
        temp->next=a;
        //指针temp每次都指向新链表的最后一个节点,其实就是 a节点,这里写temp=a也对
        temp=temp->next;
    }
    //返回建立的节点,只返回头指针 p即可,通过头指针即可找到整个链表
    return p;
}

如果想创建一个存储 {1,2,3,4} 且含头节点的链表,则 C 语言实现代码为:

link * initLink(){
    link * p=(link*)malloc(sizeof(link));//创建一个头结点
    link * temp=p;//声明一个指针指向头结点,
    //生成链表
    for (int i=1; i<5; i++) {
        link *a=(link*)malloc(sizeof(link));
        a->elem=i;
        a->next=NULL;
        temp->next=a;
        temp=temp->next;
    }
    return p;

如果使用带有头节点创建链表的方式,则输出链表的 display 函数需要做适当地修改:

void display(link *p){
    link* temp=p;//将temp指针重新指向头结点
    //只要temp指针指向的结点的next不是Null,就执行输出语句。
    while (temp->next) {
        temp=temp->next;
        printf("%d",temp->elem);
    }
    printf("\n");
}

我们只需在主函数中调用 initLink 函数,即可轻松创建一个存储 {1,2,3,4} 的链表,C 语言完整代码如下:

#include<stdlib.h>
#include<stdio.h>
//定义链表结构
typedef struct link {
    int elem;//数据域
    link *next;//指针域,指向直接后继元素
}link;//link为节点名,每个节点都是一个link结构体

//初始化无头节点链表函数
link* initNoToplink();
//创建有头节点的初始化链表
link * initlink();
//用于输出链表函数无头节点
void display(link *p);

void havadisplay(link *p);
int main() {
    //初始化链表{1,2,3,4}
    printf("初始化链表为无头节点:\n");
    link *p = initNoToplink();
    display(p);
    printf("初始化链表为有头节点:\n");
    p = initlink();
    havadisplay(p);

    return 0;
}
void havadisplay(link *p) {
    link* temp = p;//将temp指针重新指向头结点
                   //只要temp指针指向的结点的next不是Null,就执行输出语句。
    while (temp->next) {
        temp = temp->next;
        printf("%d ", temp->elem);
    }
    printf("\n");
}

//无头节点遍历链表
void display(link *p) {
    link *temp = p;//将temp指针重新指向头节点
    //判断只要temp指针指向的节点next不是null,就执行输出语句
    while (temp) {
        printf("%d ",temp->elem);
        temp = temp->next;
    }
    printf("\n");
}
//有头节点初始化
link * initlink() {
    link * p = (link*)malloc(sizeof(link));//创建一个头结点
    link * temp = p;//声明一个指针指向头结点,
                    //生成链表
    for (int i = 1; i<5; i++) {
        link *a = (link*)malloc(sizeof(link));
        a->elem = i;
        a->next = NULL;
        temp->next = a;
        temp = temp->next;
    }
    return p;
}
//无头节点初始化
link *initNoToplink() {
    link *p=NULL;//创建头指针
    link *temp = (link*)malloc(sizeof(link));//创建首元节点
    //初始化首元节点
    temp->elem = 1;
    temp->next = NULL;
    p = temp;//头指针指向首元节点
    //从第二个节点开始创建
    for (int i = 2; i < 5;i++) {
        link *a = (link*)malloc(sizeof(link));//创建一个新的节点并初始化
        a->elem = i;
        a->next = NULL;
        temp->next = a;//将temp节点与新建立的a节点建立逻辑关系
        //指针temp每次指向新链表的最后一个节点,其实就是a节点,这里temp=a;也对
        temp = temp->next;
    }
    return p;
}
View Code

运行结果为 

    

链表的基本操作

      以下对链表的操作实现均建立在已创建好链表的基础上,创建链表的代码如下所示

//声明节点结构
typedef struct Link{
    int  elem;//存储整形元素
    struct Link *next;//指向直接后继元素的指针
}link;
//创建链表的函数
link * initLink(){
    link * p=(link*)malloc(sizeof(link));//创建一个头结点
    link * temp=p;//声明一个指针指向头结点,用于遍历链表
    //生成链表
    for (int i=1; i<5; i++) {
     //创建节点并初始化
        link *a=(link*)malloc(sizeof(link));
        a->elem=i;
        a->next=NULL;
        //建立新节点与直接前驱节点的逻辑关系
        temp->next=a;
        temp=temp->next;
    }
    return p;
}

从实现代码中可以看到,该链表是一个具有头节点的链表。由于头节点本身不用于存储数据,因此在实现对链表中数据的"增删查改"时要引起注意。

链表插入元素

顺序表一样,向链表中增添元素,根据添加位置不同,可分为以下 3 种情况:

  • 插入到链表的头部(头节点之后),作为首元节点;
  • 插入到链表中间的某个位置;
  • 插入到链表的最末端,作为链表中最后一个数据元素;

虽然新元素的插入位置不固定,但是链表插入元素的思想是固定的,只需做以下两步操作,即可将新元素插入到指定的位置:

  1. 将新结点的 next 指针指向插入位置后的结点;
  2. 将插入位置前结点的 next 指针指向插入结点;

例如,我们在链表 {1,2,3,4} 的基础上分别实现在头部、中间部位、尾部插入新元素 5,其实现过程如 1 所示:

     链表中插入元素的 3 种情况示意图
                图 1 链表中插入元素的 3 种情况示意图

从图中可以看出,虽然新元素的插入位置不同,但实现插入操作的方法是一致的,都是先执行步骤 1 ,再执行步骤 2。

 注意:链表插入元素的操作必须是先步骤 1,再步骤 2;反之,若先执行步骤 2,会导致插入位置后续的部分链表丢失,无法再实现步骤 1。

 通过以上的讲解,我们可以尝试编写 C 语言代码来实现链表插入元素的操作:

//p为原链表,elem表示新数据元素,add表示新元素要插入的位置
link * insertElem(link * p,int elem,int add){
    link * temp=p;//创建临时结点temp
    //首先找到要插入位置的上一个结点
    for (int i=1; i<add; i++) {
        if (temp==NULL) {
            printf("插入位置无效\n");
            return p;
        }
        temp=temp->next;
    }   
    //创建插入结点c
    link * c=(link*)malloc(sizeof(link));
    c->elem=elem;
    //向链表中插入结点
    c->next=temp->next;
    temp->next=c;
    return  p;
}

提示,insertElem 函数中加入一个 if 语句,用于判断用户输入的插入位置是否有效。例如,在已存储 {1,2,3} 的链表中,用户要求在链表中第 100 个数据元素所在的位置插入新元素,显然用户操作无效,此时就会触发 if 语句。

链表删除元素

从链表中删除指定数据元素时,实则就是将存有该数据元素的节点从链表中摘除,但作为一名合格的程序员,要对存储空间负责,对不再利用的存储空间要及时释放。因此,从链表中删除数据元素需要进行以下 2 步操作:

  1. 将结点从链表中摘下来;
  2. 手动释放掉结点,回收被结点占用的存储空间;

其中,从链表上摘除某节点的实现非常简单,只需找到该节点的直接前驱节点 temp,执行一行程序:

 temp->next=temp->next->next;

 例如,从存有 {1,2,3,4} 的链表中删除元素 3,则此代码的执行效果如图 2 所示:

          链表删除元素示意图
                    图 2 链表删除元素示意图

 因此,链表删除元素的 C 语言实现如下所示:

//p为原链表,add为要删除元素的值
link * delElem(link * p,int add){
    link * temp=p;
    //temp指向被删除结点的上一个结点
    for (int i=1; i<add; i++) {
        temp=temp->next;
    }
    link * del=temp->next;//单独设置一个指针指向被删除结点,以防丢失
    temp->next=temp->next->next;//删除某个结点的方法就是更改前一个结点的指针域
    free(del);//手动释放该结点,防止内存泄漏
    return p;
}

我们可以看到,从链表上摘下的节点 del 最终通过 free 函数进行了手动释放。

链表查找元素

在链表中查找指定数据元素,最常用的方法是:从表头依次遍历表中节点,用被查找元素与各节点数据域中存储的数据元素进行比对,直至比对成功或遍历至链表最末端的 NULL(比对失败的标志)。

因此,链表中查找特定数据元素的 C 语言实现代码为:

//p为原链表,elem表示被查找元素、
int selectElem(link * p,int elem){
//新建一个指针t,初始化为头指针 p
    link * t=p;
    int i=1;
    //由于头节点的存在,因此while中的判断为t->next
    while (t->next) {
        t=t->next;
        if (t->elem==elem) {
            return i;
        }
        i++;
    }
    //程序执行至此处,表示查找失败
    return -1;
}

注意,遍历有头节点的链表时,需避免头节点对测试数据的影响,因此在遍历链表时,建立使用上面代码中的遍历方法,直接越过头节点对链表进行有效遍历

链表更新元素

更新链表中的元素,只需通过遍历找到存储此元素的节点,对节点中的数据域做更改操作即可。
直接给出链表中更新数据元素的 C 语言实现代码:

//更新函数,其中,add 表示更改结点在链表中的位置,newElem 为新的数据域的值
link *amendElem(link * p,int add,int newElem){
    link * temp=p;
    temp=temp->next;//在遍历之前,temp指向首元结点
    //遍历到被删除结点
    for (int i=1; i<add; i++) {
        temp=temp->next;
    }
    temp->elem=newElem;
    return p;
}

总结

以上内容详细介绍了对链表中数据元素做"增删查改"的实现过程及 C 语言代码,在此给出本节的完整可运行代码:

#include<stdio.h>
#include<stdlib.h>
//声明节点结构
typedef struct Link {
    int elem;//存储整型元素
    struct Link *next;//指向直接后继元素的指针
}Link;

//创建一个带头结点的函数
Link* initLink() {
    Link *p = (Link*)malloc(sizeof(Link));//创建一个头节点
    Link *temp = p;//声明一个指针指向头节点,用于遍历链表
    //生成链表
    for (int i = 1; i < 5;i++) {
        //创建节点并初始化
        Link* a = (Link*)malloc(sizeof(Link));
        a->elem = i;
        a->next = NULL;
        //建立新节点与直接前驱节点的关系
        temp->next = a;
        //指针temp每次都指向新链表的最后一个节点,其实就是 a节点,这里写temp=a也对
        temp = temp->next;
    }
    return p;
}
//插入元素
//p为原链表,elem为插入的新数据元素 add为插入节点的位置
Link* InsertLink(Link *p,int elem,int add) {
    Link* temp = p;//创建临时节点temp
    //首先找到要插入位置的上一个节点
    for (int i = 1; i < add;i++) {
        if (temp==NULL) {
            printf("插入位置无效\n");
            return p;
        }
        temp = temp->next;
    }
    //创建插入的节点C
    Link *c = (Link*)malloc(sizeof(Link));
    c->elem = elem;
    //向链表中插入节点
    c->next = temp->next;//先断后
    temp->next = c;//再断前
    return p;
}

//链表的删除
//p为原链表,add为要删除链表的值
Link* delLink(Link *p,int add) {
    Link* temp = p;//创建一个临时节点
    //temp指向被删除节点的上一个节点
    for (int i = 1; i < add;i++) {
        temp = temp->next;
    }
    Link *del = temp->next;//单独设置一个指针指向被删除节点,以防丢失
    temp->next = temp->next->next;//删除某个节点的方法是更改前一个节点的指针域
    free(del);//手动释放该节点防止内存泄漏
    return p;
}

//链表查找
//p为原链表,elem表示被查找元素
int selectElem(Link* p,int elem) {
     //新建一个临时指针,初始化为头指针p
    Link* t = p;
    int i = 1;
    //由于头节点的存在,因此while中的判断为t->next;
    while (t->next) {
        t = t->next;
        if (t->elem==elem) {
            return i;
        }
        i++;
    }
    //程序执行至此,表示查找失败
    printf("查找元素不存在");
    return -1;
}
//更新链表
//p表示原链表,add更改节点在链表中的位置,newElem为新的元素
Link* updataLink(Link* p,int add,int newElem) {
    Link* temp = p;//建立临时节点
    //遍历之前,temp指向首元节点
    temp = temp->next;
    //遍历到被删除的节点
    for (int i = 1; i < add;i++) {
        temp = temp->next;
    }
    temp->elem = newElem;
    return p;
}
//遍历有头节点的链表
void display(Link *p) {
    Link* temp = p;//将temp指针重新指向头结点
                   //只要temp指针指向的结点的next不是Null,就执行输出语句。
    while (temp->next) {
        temp = temp->next;
        printf("%d ", temp->elem);
    }
    printf("\n");
}

int main() {
    //初始化链表(1,2,3,4)
    printf("初始化链表为:\n");
    Link *p = initLink();
    display(p);

    printf("在第4的位置插入元素5:\n");
    p = InsertLink(p, 5, 4);
    display(p);

    printf("删除元素3:\n");
    p = delLink(p, 3);
    display(p);

    printf("查找元素2的位置为:\n");
    int address = selectElem(p, 2);
    if (address == -1) {
        printf("没有该元素\n");
    }
    else {
        printf("元素2的位置为:%d \n", address);
    }
    printf("更改第3的位置上的数据为7:\n");
    p = updataLink(p, 3, 7);
    display(p);

    return 0;
}
View Code

链表与顺序表的优缺点

1.顺序表存储(典型的数组)
     原理:顺序表存储是将数据元素放到一块连续的内存存储空间,相邻数据元素的存放地址也相邻(逻辑与物理统一)。
     优点:(1)空间利用率高。(局部性原理,连续存放,命中率高) 
                (2)存取速度高效,通过下标来直接存储。
     缺点:(1)插入和删除比较慢,比如:插入或者删除一个元素时,整个表需要遍历移动元素来重新排一次顺序。
                (2)不可以增长长度,有空间限制,当需要存取的元素个数可能多于顺序表的元素个数时,会出现"溢出"问题.当元素个数远少于预先分配的空间时,空间浪费巨大。  
  时间性能 :查找 O(1) ,插入和删除O(n)。
2.链表存储
    原理:链表存储是在程序运行过程中动态的分配空间,只要存储器还有空间,就不会发生存储溢出问题,相邻数据元素可随意存放,但所占存储空间分两部分,一部分存放结点值,另一部分存放表示结点关系间的指针。
    优点:(1)   存取某个元素速度慢。 
          (2)插入和删除速度快,保留原有的物理顺序,比如:插入或者删除一个元素时,只需要改变指针指向即可。
          (3)没有空间限制,存储元素的个数无上限,基本只与内存空间大小有关. 
    缺点:(1)占用额外的空间以存储指针(浪费空间,不连续存放,malloc开辟,空间碎片多) 
            (2)查找速度慢,因为查找时,需要循环链表访问,需要从开始节点一个一个节点去查找元素访问。
时间性能 :查找 O(n) ,插入和删除O(1)。 
*频繁的查找却很少的插入和删除操作可以用顺序表存储,堆排序,二分查找适宜用顺序表.
*如果频繁的插入和删除操作很少的查询就可以使用链表存储
*顺序表适宜于做查找这样的静态操作;链表适宜于做插入、删除这样的动态操作。
*若线性表长度变化不大,如果事先知道线性表的大致长度,比如一年12月,一周就是星期一至星期日共七天,且其主要操作是查找,则采用顺序表;若线性表长度变化较大或根本不知道多大时,且其主要操作是插入、删除,则采用链表,这样可以不需要考虑存储空间的大小问题。
*顺序表:顺序存储,随机读取
链式:随机存储,顺序读取(必须遍历)
       我们了解了两种存储结构各自的特点,那么,是否存在一种存储结构,可以融合顺序表和链表各自的优点,从而既能快速访问元素,又能快速增加或删除数据元素。

静态链表

  静态链表,也是线性存储结构的一种,它兼顾了顺序表和链表的优点于一身,可以看做是顺序表和链表的升级版。
   使用静态链表存储数据,数据全部存储在 数组中(和顺序表一样),但存储位置是随机的,数据之间"一对一"的逻辑关系通过一个整形变量(称为"游标",和指针功能类似)维持(和链表类似)。

例如,使用静态链表存储  {1,2,3} 的过程如下:
创建一个足够大的数组,假设大小为 6,如  1 所示
                                    空数组
                             图 1 空数组
 接着,在将数据存放到数组中时,给各个数据元素配备一个整形变量,此变量用于指明各个元素的直接后继元素所在数组中的位置下标,如图 2 所示:
                          静态链表存储数据
                          图 2 静态链表存储数据
通常,静态链表会将第一个数据元素放到数组下标为 1 的位置(a[1])中。
 图 2 中,从 a[1] 存储的数据元素 1 开始,通过存储的游标变量 3,就可以在 a[3] 中找到元素 1 的直接后继元素 2;同样,通过元素 a[3] 存储的游标变量 5,可以在 a[5] 中找到元素 2 的直接后继元素 3,这样的循环过程直到某元素的游标变量为 0 截止(因为 a[0] 默认不存储数据元素)。
类似图 2 这样,通过 "数组+游标" 的方式存储有线性关系数据的存储结构就是 静态链表。
 

静态链表中的节点

通过上面的学习我们知道,静态链表存储数据元素也需要自定义数据类型,至少需要包含以下 2 部分信息:
  • 数据域:用于存储数据元素的值;
  • 游标:其实就是数组下标,表示直接后继元素所在数组中的位置;
 因此,静态链表中节点的构成用 C 语言实现为:
typedef struct {
    int data;//数据域
    int cur;//游标
}component;

备用链表

图 2 显示的静态链表还不够完整,静态链表中,除了数据本身通过游标组成的链表外,还需要有一条连接各个空闲位置的链表,称为备用链表。

备用链表的作用是回收数组中未使用或之前使用过(目前未使用)的存储空间,留待后期使用。也就是说,静态链表使用数组申请的物理空间中,存有两个链表,一条连接数据,另一条连接数组中未使用的空间。

    通常,备用链表的表头位于数组下标为 0(a[0]) 的位置,而数据链表的表头位于数组下标为 1(a[1])的位置。

 静态链表中 设置备用链表的好处是,可以清楚地知道数组中是否有空闲位置,以便数据链表添加新数据时使用。比如,若静态链表中数组下标为 0 的位置上存有数据,则证明数组已满。
 例如,使用静态链表存储  {1,2,3},假设使用长度为 6 的数组 a,则存储状态可能如图 3 所示:
  图 3 中,备用链表上连接的依次是 a[0]、a[2] 和 a[4],而数据链表上连接的依次是 a[1]、a[3] 和 a[5]。
备用链表和数据链表              未存储数据之前静态链表的状态
                                                                                                                                        图 4 未存储数据之前静态链表的状态
                         图 3 备用链表和数据链表

静态链表的实现

假设使用静态链表(数组长度为 6)存储  {1,2,3},则需经历以下几个阶段。

在数据链表未初始化之前,数组中所有位置都处于空闲状态,因此都应被链接在备用链表上,如图 4 所示:
 当向静态链表中添加数据时,需提前从备用链表中摘除节点,以供新数据使用。
 备用链表摘除节点最简单的方法是摘除 a[0] 的直接后继节点;同样,向备用链表中添加空闲节点也是添加作为 a[0] 新的直接后继节点。因为 a[0] 是备用链表的第一个节点,我们知道它的位置,操作它的直接后继节点相对容易,无需遍历备用链表,耗费的 时间复杂度为  O(1)
 因此,在图 4 的基础上,向静态链表中添加元素 1 的过程如图 5 所示:
   静态链表中添加元素 1            静态链表中继续添加元素 2               静态链表中继续添加元素 3
          图 5 静态链表中添加元素 1       图 6 静态链表中继续添加元素 2                                                            图 7 静态链表中继续添加元素 3
 
 在图 5 的基础上,添加元素 2 的过程如图 6 所示:
 在图 6 的基础上,继续添加元素 3 ,过程如图 7 所示:
 由此,静态链表就创建完成了。
下面给出了创建静态链表的 C 语言实现代码:
#include<stdlib.h>
#include<stdio.h>
#define maxSize 6
typedef struct {
    int data;//数据域
    int cur;//指针域
}component;

//将结构体数据中所有分量连接到备用链表
void reserveArr(component* array);
//初始化静态链表
int initArr(component* array);
//输出函数
void displayArr(component*,int body);
//从备用链表上摘下空闲节点的函数
int mallocArr(component* array);
int main() {
    component array[maxSize];
    int body = initArr(array);
    printf("静态链表为:\n");
    displayArr(array, body);
    return 0;
}

//创建备用链表
void reserveArr(component* array) {
    for (int i = 0; i < maxSize;i++) {
        array[i].cur = i + 1;//将每个数组分量链接到一起
    }
    array[maxSize - 1].cur = 0;//链表最后一个节点的游标值为0
}
//提取分配空间
int mallocArr(component* array) {
    //若备用链表非空,则返回分配的节点下标,否则返回0
    //(当分配最后一个节点时,该节点的游标值为0)
    int i = array[0].cur;
    if (array[0].cur) {
        array[0].cur = array[i].cur;
    }
    return i;
}
//初始化静态链表
int initArr(component *array) {
    reserveArr(array);
    int body = mallocArr(array);
    //声明一个变量,把它当指针使,指向链表的最后的一个结点,因为链表为空,所以和头结点重合
    int tempBody = body;
    for (int i = 1; i<4; i++) {
        int j = mallocArr(array);//从备用链表中拿出空闲的分量
        array[tempBody].cur = j;//将申请的空闲分量链接在链表的最后一个结点后面
        array[j].data = i;//给新申请的分量的数据域初始化
        tempBody = j;//将指向链表最后一个结点的指针后移
    }
    array[tempBody].cur = 0;//新的链表最后一个结点的指针设置为0
    return body;
}
void displayArr(component * array, int body) {
    int tempBody = body;//tempBody准备做遍历使用
    while (array[tempBody].cur) {
        printf("%d,%d ", array[tempBody].data, array[tempBody].cur);
        tempBody = array[tempBody].cur;
    }
    printf("%d,%d\n", array[tempBody].data, array[tempBody].cur);
}
View Code

提示,此代码创建了一个带有头节点的静态链表,因此最先输出的 ",2" 表示的是头节点,其首元节点(存储元素 1 的节点)在数组 array[2] 中。

     双向链表 

  无论是 静态链表还是 动态链表,有时在解决具体问题时,需要我们对其结构进行稍微地调整。比如,可以把链表的两头连接,使其成为了一个环状链表,通常称为循环链表。
 和它名字的表意一样,只需要将表中最后一个结点的指针指向头结点,链表就能成环儿,如  1 所示。

            
                          图1 循环链表
需要注意的是,虽然循环链表成环状,但本质上还是链表,因此在循环链表中,依然能够找到头指针和首元节点等。循环链表和普通链表相比,唯一的不同就是循环链表首尾相连,其他都完全一样
 

循环链表实现约瑟夫环

约瑟夫环问题,是一个经典的循环链表问题,题意是:已知 n 个人(分别用编号 1,2,3,…,n 表示)围坐在一张圆桌周围,从编号为 k 的人开始顺时针报数,数到 m 的那个人出列;他的下一个人又从 1 开始,还是顺时针开始报数,数到 m 的那个人又出列;依次重复下去,直到圆桌上剩余一个人。

如图 2 所示,假设此时圆周周围有 5 个人,要求从编号为 3 的人开始顺时针数数,数到 2 的那个人出列:
                循环链表实现约瑟夫环
                  图 2 循环链表实现约瑟夫环
出列顺序依次为:
  • 编号为 3 的人开始数 1,然后 4 数 2,所以 4 先出列;
  • 4 出列后,从 5 开始数 1,1 数 2,所以 1 出列;
  • 1 出列后,从 2 开始数 1,3 数 2,所以 3 出列;
  • 3 出列后,从 5 开始数 1,2 数 2,所以 2 出列;
  • 最后只剩下 5 自己,所以 5 胜出。
 约瑟夫环问题有多种变形,比如顺时针转改为逆时针等,虽然问题的细节有多种变数,但解决问题的中心思想是一样的,即使用循环链表。
 通过以上的分析,我们可以尝试编写 C 语言代码,完整代码如下所示:
#include <stdio.h>
#include <stdlib.h>
typedef struct node{
    int number;
    struct node * next;
}person;
person * initLink(int n){
    person * head=(person*)malloc(sizeof(person));
    head->number=1;
    head->next=NULL;
    person * cyclic=head;
    for (int i=2; i<=n; i++) {
        person * body=(person*)malloc(sizeof(person));
        body->number=i;
        body->next=NULL; 
        cyclic->next=body;
        cyclic=cyclic->next;
    }
    cyclic->next=head;//首尾相连
    return head;
}
void findAndKillK(person * head,int k,int m){
    person * tail=head;
    //找到链表第一个结点的上一个结点,为删除操作做准备
    while (tail->next!=head) {
        tail=tail->next;
    }
    person * p=head;
    //找到编号为k的人
    while (p->number!=k) {
        tail=p;
        p=p->next;
    }
    //从编号为k的人开始,只有符合p->next==p时,说明链表中除了p结点,所有编号都出列了,
    while (p->next!=p) {
        //找到从p报数1开始,报m的人,并且还要知道数m-1de人的位置tail,方便做删除操作。
        for (int i=1; i<m; i++) {
            tail=p;
            p=p->next;
        }
        tail->next=p->next;//从链表上将p结点摘下来
        printf("出列人的编号为:%d\n",p->number);
        free(p);
        p=tail->next;//继续使用p指针指向出列编号的下一个编号,游戏继续
    }
    printf("出列人的编号为:%d\n",p->number);
    free(p);
}
int main() {
    printf("输入圆桌上的人数n:");
    int n;
    scanf("%d",&n);
    person * head=initLink(n);
    printf("从第k人开始报数(k>1且k<%d):",n);
    int k;
    scanf("%d",&k);
    printf("数到m的人出列:");
    int m;
    scanf("%d",&m);
    findAndKillK(head, k, m);
    return 0;
}
View Code

输出:

     

最后出列的人,即为胜利者。当然,你也可以改进程序,令查找出最后一个人时,输出此人胜利的信息。

总结

  循环链表和动态链表唯一不同在于它的首尾连接,这也注定了在使用循环链表时,附带最多的操作就是遍历链表。
在遍历的过程中,尤其要注意循环链表虽然首尾相连,但并不表示该链表没有第一个节点和最后一个结点。所以,不要随意改变头指针的指向。

猜你喜欢

转载自www.cnblogs.com/2019wxw/p/10804610.html