数据结构学习总结(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;
}

运行结果为

    

链表的基本操作

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

//声明节点结构
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 所示:

    
                图 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;
}

链表与顺序表的优缺点

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 所示:

                           

          图 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);
}

提示,此代码创建了一个带有头节点的静态链表,因此最先输出的 ",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;
}

输出:

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

总结

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

原文地址:https://www.cnblogs.com/2019wxw/p/10804610.html

时间: 2024-08-02 12:52:01

数据结构学习总结(2) 线性表之单链表的相关文章

[大话数据结构]线性表之单链表结构和顺序存储结构

线性表定义: 零个或者多个数据元素的有限序列.元素之间是有顺序的,如果元素存在多个,则第一个元素无前驱,最后一个元素无后继.其他每个元素都有且只有一个前驱和后继.并且数据元素的类型要相同. 线性表的抽象数据类型: ADT 线性表(List) Data 线性表的数据对象集合为{a1,a2,...,an},每个元素的类型均为DataType. 其中,除第一个元素a1外,每一个元素有且只有一个直接前驱元素,除了最后一个元素an外,每一个元素有且只有一个直接后继元素. 数据元素之间的关系是一对一的关系.

续上文----线性表之单链表(C实现)

本文绪上文线性表之顺序表(C实现) 本文将继续使用单链表实现线性表 的另外一种存储结构.这种使用 链表实现的存储结构在内存中是 不连续的. C实现代码如下: #include<stdio.h> typedef struct node { int data; struct node *next; }Node; //链表的初始化 Node* InitList(int number) { int i; Node *pHead=(Node *)malloc(sizeof(Node)); Node *T

数据结构学习系列之线性表(二)

前言 线性表链式存储结构的实现,通过这种方式实现的线性表,简称为链表,这是这篇文章的主题.与顺序存储相对应的是链式存储.链式存储逻辑结构相邻,物理结构可能相邻也有可能不相邻.链式结构的优点有:1.存储空间不限制(操作系统可支持的存储空间范围内):2.插入删除操作不需要移动元素等等.当然链式结构也有缺点,比如每个节点需要维护指向下一个节点的指针:比如需要查找某个节点时,需要从头节点开始查找,时间复杂度O(n)等等.总之,顺序存储以及链式存储各有优缺点,需要根据需求具体情况,选择合适的存储方式.没有

【Java】 大话数据结构(2) 线性表之单链表

本文根据<大话数据结构>一书,实现了Java版的单链表. 书中的线性表抽象数据类型定义如下(第45页): 实现程序: package LinkList; /** * 说明: * 1.<大话数据结构>中没有线性表的长度,但提到可以存储于头节点的数据域中. * 本程序的线性表长度存放于count变量中,线性表长度可以使程序比较方便. * 2.程序中,第i个位置代表第i个结点,头结点属于第0个结点 * 3.因为链表为泛型,整表创建采用整型(随机整数做元素),所以有出现一些类型转换 * 4

线性表之单链表学习小结(初学数据结构必看)

花了好几个小时,详细规划出了整个过程,包括所有基本操作...有什么疑问请下方留言 #include<iostream> using namespace std; #define ElemType char #define ERROR 0 #define OK 1 typedef struct Node { ElemType data; struct Node *next; }Node,*LinkList; void init_linklist(LinkList L)/*对单链表进行初始化*/

数据结构:线性表之单链表

线性表(亦作顺序表)是最基本.最简单.也是最常用的一种数据结构.线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的.线性表有两种存储结构: ①顺序存储结构,即存储单元在一段连续的地址上存储,常见的数组就是顺序存储结构的线性表: ②链式存储结构,即存储单元在不连续的地址上存储.因为其不连续性,除了要存数据元素信息(数据域)外,还要存储它后继元素(结点)的地址(指针域,链).学习链式结构最好将结点结构牢记于心,如下图: 链表的每个结点只含有一个指

Java数据结构-线性表之单链表LinkedList

线性表的链式存储结构,也称之为链式表,链表:链表的存储单元可以连续也可以不连续. 链表中的节点包含数据域和指针域,数据域为存储数据元素信息的域,指针域为存储直接后继位置(一般称为指针)的域. 注意一个头结点和头指针的区别: 头指针: 指向链表的第一个节点的指针,若链表有头结点,则是指向头结点的指针: 头指针具有标识作用,所以常用头指针作为链表的名字: 不论链表是否为空,头指针都不为空: 是链表的必要元素. 头结点: 头结点是为了操作的统一和方便而设立的,放在第一个元素节点的前面,其数据域一般无意

数据结构学习系列之线性表(三)

前言数据结构学习,发现坚持下来比较难,本次学习与上次学习间隔了几个月,不管怎样还是要强迫自己坚持下来. 静态链表 用数组模拟链式结构的线性表,对于没有指针的编程语言,只能使用这种方式来模拟.这是大师们想出来的实现方法,体会大师们的编程思路,站在大师们的肩膀上解决一个又一个的难题.每个节点包含一个游标(数组数字索引),用于指向下个节点,所以静态链表也称为游标链表.规定数组第一个元素为备用链表(未被使用的数组游标,静态链表初始化时,会生成备用链表)的头节点,数组最后一个元素为链表的头节点.当需要插入

数据结构学习系列之线性表(五)

前言 单向链表查找下一个元素很方便,要查找上一个元素时,需要从头开始向下遍历,很是麻烦.如何解决这个问题呢?使用双向链表结构可以解决这个问题. 双向链表 在单向链表的基础上,增加一个指向上一个节点的指针,这就形成了双向链表结构.因增加了一个指针域,故需要占用更多的内存空间,换就话说,用空间换时间.现在硬件越来越强,而价格越来越低,这种思想的应用案例越来越普遍. 代码实现 1 /** 2 * @desc 双向链表 3 * 4 * @date 2015/08/23 5 * @copyright by