C语言中的特殊结构类型“单链表”

Posted 天“码”行空

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言中的特殊结构类型“单链表”相关的知识,希望对你有一定的参考价值。

 

要建立一个班级的学生信息表,根据之前所学的知识,用结构数组(数组元素为结构)来实现。例如如下代码:

typedef struct student 

   char ID[10];
   char name[10];
   char sex;
   float score;
STUDENT;

STUDENT stu[30] = 
     "202001", "张三", 'M', 85
     "202002", "李四", 'F', 90  
              ;               

数组是最基本的构造类型,它是一组相同类型数据的有序集合。数组中的元素在内存中连续存放。学过数据结构这门课程,就会知道,数组实质是一种线性结构的顺序表示方式。即数组最大特点是连续,顺序。这样方便存取线性表中的任意一元素。但是我们也会发现数组作为数据类型的一些缺点,比如说,我要插入一个元素时,造成后面所有元素后移一位。而删除一个元素时,造成后面所有元素前移一位。同时应该注意到,我们在定义数组时要指定数组长度,定义数组长度过小时造成实际使用的数组元素个数超过最大长度发生溢出,而定义的数组长度过大造成系统资源的浪费。

既然数组由这些缺点,哪么有没有一种物理存储结构上非连续、非顺序的存储结构呢?我们今天要学习的链表就是这样的一种结构。

一.单链表类型定义

单链表是由一连串的结构(称为结点)组成的,其中每个结点都包含指向下一结点的指针(单链表最后一个结点因其没有下一个结点,故单链表最后一个结点的指针为空)。即一般情况下,每个结点包括两部分,一部分是数据域,存储相关数据;另一部分是指针域,存取下一结点的地址(最后一个结点没有指针域)。

在此区别结构数组,结构数组也是由一连串的结构组成,但是这种组成是连续的,有顺序的。而且结构数组中每个结构是没有指针域的,因为其是顺序存放的,不需要指明下一结构的地址。

逻辑图如下图所示:

 

总结:顺序存储结构要求所有元素在内存中相邻存放,因而需占用一段连续的存储空间;而链式存储结构不是这样,每个结点单独存储,无需占用一整块存储空间,但是为了表示结点之间的关系,每个结点的后面附加指针(用于存放下一结点的存储地址),指向下一结点。

接下来要为链表中的结点声明类型,简单起见,先假设结点只包含一个int型整数(即结点的数据域)和指向表中下一结点的指针(指针域),则结点的结构可如下描述:

struct node //单链表的本质是一连串的结构,所以用关键字struct

   int data;//结构包括数据域和指针域
   struct node *next;//定义一个指针变量,存放一个结点的地址。即指向下一结点的指针

上面定义了一个名为node(结点)的结构类型,该结构包含两个成员,第一个是存放实际数据的整型值data,第二个是指向node结构类型的指针next,功能是指向下一结点。

由前面的单链表的逻辑图可知,链表中的结点是通过next指针依次连接在一起的,最后一个结点的指针为空,即值为NULL。也就是说找到第一个结点,顺着每个结点的指针就能找到下一个结点,直到找到最后一个结点。这时,我们自然会问,第一个结点又是如何标识的呢?只有找到第一个结点才能顺着指针直到找到最后一个结点。这是通过一个称为头指针(head)的特殊指针标识的。头指针总是指向链表的第一个结点。当然也有一种特殊情况就是整个链表为空(即不包含任何结点),则将头指针置为NULL。

为了操作方便,往往在首元结点前再增加一个结点,称为头结点,头结点不存储元素。头指针指向头结点,头结点的指针域指向首元结点,我们称这种单链表为带头结点的单链表。链表为空时,头结点的指针域为空。

单链表是由一连串的结构(称为结点)组成。所以形象地说,单链表好比将数据域和指针域打包形成一个个包裹,并在每个包裹中有下一包裹在哪个地方的线索,主人只要找到第一个包裹就能找到所有属于他的包裹。主人怎么找到自己的第一个包裹呢?是一种称为头指针的特殊指针,这个头指针就好比是取快递单号,这里跟生活中取包裹还有些不一样,拿着它不能告诉你包裹是具体哪个地方,只能告诉你位于哪一个货架上,这个货架就相当于头结点,货架上会有你的名字,而你的第一个包裹就在你的名字下面。由此可知,一个人收到快递单号(头指针),上面告诉你,你的第一个包裹位于哪个货架上(头指针指向头结点),货架(头结点)上有你的名字(头结点的指针域),下面对应你第一个包裹(首元结点),通过第一个包裹,可以依次找到你的所有包裹(后续结点)了。

 

单链表的结构图以及一些名词(将说明中指针域更正为数据域)

注明:尾结点指针域为空,值为NULL。

二.单链表的操作

创建一个单链表之后可以对链表进行遍历,增加,删除,修改等各种操作。正如前面所说,链表中的结点是通过指针连接起来的,因此增加和删除结点的大部分工作是处理指针的指向。结点可能被增加到链表头,中间或末尾,这决定了应该如何修改指针。接下来,我们用前面定义的node结构详细介绍单链表的常见操作。

1.单链表的遍历

在单链表中,如何顺序输出单链表的所有元素?由于单链表的结构没有包含表长信息,所以事先不知道要循环多少次,不方便使用for循环。其核心思想是,工作指针后移,直到链表结束。算法思路如下:

【1】声明一个指针p,并指向首元结点(把头结点的指针域(首元结点的地址)赋值给p);

//指向谁,就是把谁的地址赋值给指针变量,而“谁”的地址保存在“谁”前面那个结点的指针域中,即把前一结点的指针域赋值给指针变量

【2】当p不为空的时候,重复如下两个操作:

  输出p所指结点中的元素(p---->data);

  指针p后移指向下一个元素(p = p----->next)。

//定义一个函数,实现单链表的遍历,输出链表中所有元素
void printlist(struct node *head)//形参为指向头结点的头指针

   struct node *p = head->next;  //head->next访问头结点的地址域,其为下一个结点(首元结点)的地址,现在把首元结点的地址赋给指针变量p,表明p指向首元结点
 //head为结构指针变量名,next为结构成员名,可用结构指针变量名->成员名访问结构成员
   while(p)//p不为空,则返回True
   
       printf("%d ", p->data);  //表示访问p所指结点的数据域
       p = p->next;            //并把p所指的地址域重新赋值给p,p所指的地址域恰好是下一结点的地址,即p指向下一结点
    

对结点的一些思考感悟:结点的本质是结构,只不过这个结构比较特殊,它不单包含数据,还包含下个结点的地址。多个结点连在一起,构成单链表。既然结点也是结构,那么可以用结构指针变量名----->成员名来对指针变量所指结构的成员进行访问。如head----->next表示访问head指针(头指针)所指结构(结点)的成员next,而next被定义为一个指针变量,其保存的是下一结点的地址。所以head----->next表示头结点后一结点的地址,即首元结点的地址。在此要理清结点,结构,指针域之间的关系以及如何用箭头运算符对结构成员的访问。

2.单链表的建立

链表的建立是从无到有,一个一个地构建结点,并建立前后链接关系的一个过程。建立单链表的主要操作步骤如下:

【1】定义单链表的数据结构;

【2】读取数据;

【3】生成新结点;

【4】将数据存入结点的成员变量中;

【5】将新结点连接到表头或表尾。

重复【2】----【5】直到结束。

从上述过程可以看出,建立单链表的实质就是根据需要一个一个地构建新结点,在结点中存放数据并建立结点之间的链接关系。根据新结点插入位置不同,单链表的建立分为头插法和尾插法两种方式。

【1】头插法建立单链表

四步走】顾名思义,每次从表头插入新结点,具体过程:先开辟头指针(head),head后有头结点,头结点中的指针域保存的是首元结点的地址。那如何表示头结点的指针域呢?可以用head----->next表示。然后动态开辟第一个结点。有了第一个结点后,要把第二个结点放在第一个结点的前面,也就是说把第一个结点作为第二个结点的后继结点。想一想,应该做什么工作呢?【1】malloc()动态创建一块内存区域表示一个结点,定义一个结构指针变量p,指向所分配内存空间的起始地址的指针,即p指向所创建的新结点。【2】新结点的指针域保存的应该是上一个结点的地址。而上一个结点在新结点没有被创建之前是作为首元结点的,所以上一个结点的地址保存在头结点的指针域中(head---->next),现在上一结点的地址应该保存在动态创建的新结点的指针域中,其过程为把上一结点的地址赋值给动态创建的结点的指针域,即p---->next = head-------next;【3】把一个整数num赋值给新结点的数据域p----->data。【4】指针变量p保存的是新结点的地址,故新结点又作为首元结点,把新结点的地址保存在头结点的指针域中(head ----> next = p)。

使用头插法构建一个单链表

//使用一个函数,实现头插法建立单链表
void CreateList_H(struct node *head)//头指针作为函数的形参

   struct node *p;//定义结构指针变量p
   int num;
   
   printf("输入若干整数,输入-1表示输入结束\\n");
   while(scanf("%d",&num), num != -1)
   
      p = (struct node *)malloc(sizeof(struct node));  //动态创建新结点,p保存这个结点的地址
      p->data = num ;//将数据存入新的结点data域
      p->next = head->next;//指针域存放原来首元结点的地址,使原来的首元结点在新创建的结点后面
      head->next = p; //将新结点的地址保存到头结点的指针域
   
      

总结:头结点指针域保存新结点地址,新结点的指针域保存原来首元结点的地址,这种方法叫头插法

【2】尾插法建立单链表

每次从表尾插入结点,具体过程:【1】先开辟头指针,把它作为形参。【2】定义两个结构指针变量,其中一个结构指针变量p指向新创建的结点,而另外一个结构指针变量则总是指向链表的最后一个结点。即rear总是保存最后一个结点的地址。【3】malloc()动态创建一块内存区域表示一个结点。【4】把一个整数num赋值给新结点的数据域p----->data。【5】把新结点的地址保存在上一结点的指针域中,上一结点的指针域可以用rear------>next来表示。【6】更新rear,使得总是指向最后一结点

void CreateList_T(struct node *head)

    struct node *p, *rear;//rear总是指向链表的最后一个结点
    rear = head;//把head保存的地址赋值给rear,否则rear的值就是垃圾值
    int num;

    printf("输入若干整数,输入-1表示输入结束\\n");
    while(scanf("%d",&num), num != -1)
    
      p = (struct node *)malloc(sizeof(struct node));  //动态创建的新结点,p保存这个结点的地址
      p->data = num;
      rear->next = p;//将新结点作为rear的后继
      rear = p;      //更新rear
     
     rear->next = NULL;//表尾结点的后继置为NULL

总结:定义两个指针变量,其中一个指针变量总是指向链表最后一个结点。把动态新开辟的结点的地址赋值给末尾结点的指针域,并且不断更新其中一个指针变量,使其总是指向链表最后一个结点。

3.单链表的插入

步骤:

【1】在链表中确定新元素应该被加入到哪个结点的后面,该结点被称为前驱结点,用指针变量pre指向;

【2】创建一个新结点,使用一个指针p指向它;

【3】将新结点的next指针指向前驱结点next指针指向的那个结点;

【4】将前驱结点的next指针指向新结点。

新结点插入的实质是,将新结点插入后 其前驱结点的指针域存放新结点的地址,而新结点的指针域存放后继结点的地址,从而实现单链表的插入。

//定义一个函数ListInsert(),构造一个值为e的结点p,作为链表head的第i个结点
void ListInsert(struct node *head, int i, int e)

   struct node *p, *pre;
   pre = head;   //让pre指向头结点
   int j = 0;

   while(pre && j < i-1)  //如果pre非空且pre不是第i-1个结点
   
      pre = pre---->next; j++;  
    

    if( !pre || i < 1)     //如果链表结束或i<1
    
        printf("i值错误\\n");
        return;
    
    p = (struct node *)malloc(m);  //生成新结点s
    p---->data = e;
    p---->next = pre---->next;   //让原来pre的后继结点做结点p的后继结点
    pre---->next = p;         //结点p作为pre的后继

四.单链表的删除

把数据存储到链表中一个最大的好处就是可以轻松删除不需要的结点。删除结点包含以下步骤:

【1】找到要删除的结点(用p指向这个结点)和它的前驱结点(用pre指向这个结点);

【2】改变前驱结点的next指针,从而使他绕过被删除的结点;

【3】调用free函数释放删除结点占用的内存空间。

//函数ListDelete()删除链表head的第i个结点,并将第i个结点的值存入形参指针e所指的内存
void ListDelete(struct node *head, int i, ElemType *e)

    struct node *p , *pre;
    pre = head;
    int j = 0;

    while(pre && j < i-1)//pre指向第i-1个结点
    
       pre = pre--->next;
       ++j;
    
    if(!pre || i < 1)  //如果链表结束或i<1
    
       printf("i值错误\\n");
       return;
    
    
    p = pre---->next;  //p指向要被删除的结点
    pre---->next = p---->next; //绕过要删除的结点p
    *e = p---->next;     //把p的数据存入e所指的内存
    free(p);    //释放被删除的结点占用的内存空间

 
    
       

再次总结,单链表的操作包括遍历,构建,插入,删除。

 

以上是关于C语言中的特殊结构类型“单链表”的主要内容,如果未能解决你的问题,请参考以下文章

2-12在一个单链表head中,若要在指针p所指结点后插入一个q指针所指结点,则执行()?

C语言中数据结构中的单向链表的问题;

单链表浅析

数据结构-单链表模拟实现循环队列(c语言实现)

Python线性表——单链表

嵌入式 Linux C语言——单链表