数据结构之单链表(C语言)
- 1 链表的概念
- 2 节点创建函数与链表打印函数
- 3 单链表尾插法与头插法
- 4 单链表尾删法与头删法
- 5 指定位置的插入与删除
- 5.1 在指定位置之前插入数据
- 5.2 在指定位置之后插入数据
- 5.3 删除指定位置节点
- 5.4 删除指定位置之后节点
- 6 链表数据的查找与链表的销毁
- 7 单链表全程序及演示结果
- 7.1 SList.h 文件
- 7.2 SList.c 文件
- 7.3 SList_test.c 文件
1 链表的概念
链表是线性表的一种,其物理存储结构上是非连续、非顺序的存储结构,数据元素的逻辑存储结构是通过链表中指针链接次序实现的。即链表中:
(1)物理结构:不是线性的
(2)逻辑结构:一定是线性的
链表是由一个一个节点(结点)构成的,可以类比春运前与春运后火车的车厢数量,每一个车厢都可看作是一个节点,春运时就增加车厢数(节点数)来运送更多乘客(存储更多数据),非春运时就减少车厢数,可保证基本运作即可。
单链表中每一个节点是由两部分组成,第一是该节点所要存储的数据,第二是该节点所指向的下一个节点的指针(地址)。

具体代码如下:
typedefintSLTDataType;typedefstructSListNode{ SLTDataType data;structSListNode*next;}SLTNode;
注意,在指向下一节点指针这里,它的类型之所以不能使用重命名后的结构体类型SLTNode
是因为在系统读取到该行时还没完成重命名的操作,故只能使用重命名前的名称struct SListNode
来定义类型。
2 节点创建函数与链表打印函数
2.1 节点创建函数
在上面我们只是定义了节点的结构,但并没有真正创建节点。故我们自定义一个函数来完成这部分操作。既然要创建节点,那么就需要申请空间,因为此时不同于数组扩容,链表的物理结构并不是连续的,而是可以通过指针将每一个碎片化的节点连接起来,所以直接使用malloc
来进行空间申请即可。这一部分与顺序表中的空间检查函数很相似。
SLTNode*SLTBuyNode(SLTDataType x){ SLTNode*newnode =(SLTNode*)malloc(sizeof(SLTNode));if(newnode ==NULL){ perror("malloc fail !");exit(1);}newnode->data =x;newnode->next =NULL;returnnewnode;}
2.2 链表打印函数
在上文中我们已知,链表是靠指针来完成次序的连接,其无法做到任意访问,而是必须先访问前一个节点后再去访问后一个节点。比如,我们将第一个节点所对应的节点地址设为phead
,那么下一个节点就是phead->next
,再后面一个节点为phead->next->next
,以此类推。
在上面的那种访问方式中,当我们要访问第n个节点时,phead
后面会跟(n-1)个next
。如此就显得非常冗余,所以我们可以另辟蹊径,用一个临时节点来做为过度节点(我们设为pcur
),将链表的头节点赋值给pcur
,然后通过循环让pcur
不断指向后面的next
完成对链表的遍历。实现如下:

SLTPrint(SLTNode*phead){ SLTNode*pcur =phead;while(pcur){ printf("%d->",pcur->data);pcur =pcur->next;}printf("NULL\n");}
3 单链表尾插法与头插法
3.1 尾插函数
在原链表之尾插入一个新的节点,首先我们要调用节点创建函数建立一个新的节点,然后对原链表进行遍历找尾,最后将尾节点的next
指向新创建的节点即可。
请审读下面尾插函数,分析其是否存在问题。
SLTNode*PushBack(SLTNode*phead,SLTDataType x){ assert(phead);SLTNode*newnode =SLTBuyNode(x);SLTNode*ptail =phead;while(ptail->next){ ptail =ptail->next;}ptail->next =newnode;}
上面尾插程序看似十分完美,其实存在两点严重的问题:
- 当链表为空链表时,
phead
与ptail
都会成为空指针NULL
,那么在while
中的条件判断就成为了对空指针解引用的非法访问。 - 在尾插函数中,我们想通过修改形参中的链表来完成实参中链表的修改,那么在形参中可以用一级指针接收传参吗?用一级指针接收传参可以保证实参改变吗?
我们先来解决第一个问题,既然有空链表的这种情况,那么就使用一个条件分支来区别二者(空链表与非空链表)。
voidPushBack(SLTNode*phead,SLTDataType x){ assert(phead);SLTNode*newnode =SLTBuyNode(x);if(phead ==NULL){ phead =newnode;}else{ SLTNode*ptail =phead;while(ptail->next){ ptail =ptail->next;}ptail->next =newnode;}}
然后我们再来解决第二个问题,形参如果想要影响实参的内容,我们传参时就要传地址而不是传数值(即使用传址调用)。测试文件中内容如下:
voidTest01(){ SLTNode*plist =NULL;SLTDataType x =1;PushBack(plist,x);SLTPrint(plist);}intmain(){ Test01();return0;}
在传参时我们是将plist
作为实参,而plist
是一个一级指针,其存储的是后面的空地址NULL
。所以在传参时phead
所接收的是plist
所指向的NULL
,属于传值调用。在前面函数已经讲过,传值调用时函数会在栈区开创栈帧空间来存储形参及函数内参数,也就是在栈帧空间内某一块空间被置为NULL
,然后再由phead
来存储。此时无论phead
怎么改变,都与原来的plist
毫无关系了,形参也就无法影响实参了。
所以在传参时我们不能传plist
(等于传值),而是要传plist
的地址(&plist
)。相应的,因为传递的是一级指针的地址,所以在形参部分接收头节点的形参要使用二级指针(我们将其命名为pphead
)。
最终尾插函数如下:
voidPushBack(SLTNode**pphead,SLTDataType x){ assert(pphead);SLTNode*newnode =SLTBuyNode(x);if(*pphead ==NULL){ *pphead =newnode;}else{ SLTNode*ptail =*pphead;while(ptail->next){ ptail =ptail->next;}ptail->next =newnode;}}

在经过上面修改后,传参时一级指针的地址&plist
作为实参,形参处的二级指针pphead
来接收实参,形参实例化后开创栈帧来存储一级指针地址&plist
,后续我们对pphead
解引用操作时(*pphead
),操作的就是测试文件中的一级指针plist
,完成形参改变实参的操作。
3.2 头插函数
在原链表之头插入一个节点,我们在调用节点创建函数后只需要将新节点newnode->next赋值为存储链表头节点地址的一级指针plist,然后再将一级指针newnode赋值给plist就完成了头节点的转换。
具体如下:
voidPushFront(SLTNode**pphead,SLTDataType x){ assert(pphead);SLTNode*newnode =SLTBuyNode(x);newnode->next =*pphead;*pphead =newnode;}
4 单链表尾删法与头删法
4.1 尾删函数
我们先来思考一下,能否从头遍历链表后直接free
掉尾节点?
答案肯定是不能的。链表的每一节点间都通过next
指针相连,若直接free
掉尾节点,那么尾节点前面一个节点的next
指针就会指向一块未初始化的空间,成为野指针。所以我们需要定义两个指针,一个是尾节点ptail
,另一个是尾节点前面一个节点prev
,free
掉尾节点后,将ptail
置为NULL
,再将prev->next
置为NULL
。
voidPopBack(SLTNode**pphead){ assert(pphead &&*pphead);SLTNode*prev =*pphead;SLTNode*ptail =*pphead;while(ptail->next){ prev =ptail;ptail =ptail->next;}free(ptail);ptail =NULL;prev->next =NULL;}
在上面的逻辑中,情况是否全面不妨和前面一样,使用极端情况来检验。空链表的情况下会被assert断言发现,故被排除。那在只有一个节点时呢?因为ptail->next为空,故不进入循环。直接free并置为NULL,所以ptail与prev都为NULL。但是后面prev->next却又对空指针进行了非法访问,造成出错。故应在逻辑中分为链表只有一个节点和有多个节点。一个节点时直接free并置空,多个节点使在执行上面程序。
最终程序如下:
voidPopBack(SLTNode**pphead){ assert(pphead &&*pphead);SLTNode*prev =*pphead;SLTNode*ptail =*pphead;if((*pphead)->next ==NULL){ free(*pphead);*pphead =NULL;}else{ while(ptail->next){ prev =ptail;ptail =ptail->next;}free(ptail);ptail =NULL;prev->next =NULL;}}
4.2 头删函数
删掉单链表头节点只需要将头节点的next指针指向地址保留下来,释放头节点空间,然后将方才保留的地址转换为头节点地址即可。
voidPopFront(SLTNode**pphead){ assert(pphead &&*pphead);SLTNode*next =(*pphead)->next;free(*pphead);*pphead =next;}
5 指定位置的插入与删除
5.1 在指定位置之前插入数据
在指定位置之前插入数据,首先我们思考一下,指定位置若是头节点,此时其就是头插法插入(特殊情况)。除此之外,我们若在指定位置之前直接插入新节点,那么指定位置之前原来的节点就会与pos节点断开。所以我们需要找到前一个节点地址,将前一个节点next指针指向newnode,newnode->next指向pos节点,如此完成连接。
具体见下:
voidSLTInsert(SLTNode**pphead,SLTNode*pos,SLTDataType x){ assert(pphead &&*pphead);assert(pos);SLTNode*newnode =SLTBuyNode(x);if(pos ==*pphead){ PushFront(pphead,x);}else{ SLTNode*prev =*pphead;while(prev->next !=pos){ prev =prev->next;}newnode->next =pos;prev->next =newnode;}}
5.2 在指定位置之后插入数据
在指定位置之后插入数据我们通过前文的讲述,自然会先考虑到尾插的情况,在pos
在尾节点时,pos->next == NULL
,我们将pos->next
直接赋值给newnode->next
,然后将pos
与newnode
连接即可(pos->next==newnode
)。在尾插情况之外,通过上面的连接操作同样可以完成插入数据,故不需要使用条件分支。
具体见下:
voidSLTInsertAfter(SLTNode**pphead,SLTNode*pos,SLTDataType x){ assert(pphead &&*pphead);assert(pos);SLTNode*newnode =SLTBuyNode(x);newnode->next =pos->next;pos->next =newnode;}
5.3 删除指定位置节点
在pos节点为头结点,此时删除操作符合头删法,即调用头删函数即可。除此情况之外,我们需要将pos前一个节点与后一个节点连接后再释放pos节点。
具体见下:
voidSLTErase(SLTNode**pphead,SLTNode*pos){ assert(pphead &&*pphead);assert(pos);if(pos ==*pphead){ PopFront(pos);}else{ SLTNode*prev =*pphead;while(prev->next !=pos){ prev =prev->next;}prev->next =pos->next;free(pos);pos =NULL;}}
5.4 删除指定位置之后节点
与前面操作如出一辙,我们将pos与其后两个节点连接,最后再释放pos后一个节点置空即可。但在此处我们需要考虑pos
是不是尾节点,即pos->next
是否等于NULL
。
具体见下:
voidSLTEraseAfter(SLTNode**pphead,SLTNode*pos){ assert(pphead &&*pphead);assert(pos &&pos->next);SLTNode*del =pos->next;pos->next =del->next;free(del);del =NULL;}
6 链表数据的查找与链表的销毁
6.1 链表数据的查找
与顺序表的查找如出一辙,对整个链表遍历即可。找到就返回该节点,没找到就返回NULL
。
SLTNode*SLTFind(SLTNode*phead,SLTDataType x){ SLTNode*pcur =phead;while(pcur){ if(pcur->data ==x){ returnpcur;}pcur =pcur->next;}returnNULL;}
6.2 链表的销毁
在对链表每个节点释放之前,我们需要先保留该节点的next指针的指向,从而能够销毁后续节点。
voidSLTDestroy(SLTNode**pphead){ assert(pphead &&*pphead);SLTNode*pcur =*pphead;while(pcur){ SLTNode*next =pcur->next;free(pcur);pcur =next;}*pphead =NULL;}
7 单链表全程序及演示结果
7.1 SList.h 文件
#pragmaonce#include#include#includetypedefintSLTDataType;typedefstructSListNode{ SLTDataType data;structSListNode*next;}SLTNode;SLTNode*SLTBuyNode(SLTDataType x);voidSLTPrint(SLTNode*phead);voidPushBack(SLTNode**pphead,SLTDataType x);voidPushFront(SLTNode**pphead,SLTDataType x);voidPopBack(SLTNode**pphead);voidPopFront(SLTNode**pphead);voidSLTInsert(SLTNode**pphead,SLTNode*pos,SLTDataType x);voidSLTInsertAfter(SLTNode**pphead,SLTNode*pos,SLTDataType x);voidSLTErase(SLTNode**pphead,SLTNode*pos);voidSLTEraseAfter(SLTNode**pphead,SLTNode*pos);SLTNode*SLTFind(SLTNode*phead,SLTDataType x);voidSLTDestroy(SLTNode**pphead);
7.2 SList.c 文件
#include"SList.h"SLTNode*SLTBuyNode(SLTDataType x){ SLTNode*newnode =(SLTNode*)malloc(sizeof(SLTNode));if(newnode ==NULL){ perror("malloc fail !");exit(1);}newnode->data =x;newnode->next =NULL;returnnewnode;}voidSLTPrint(SLTNode*phead){ SLTNode*pcur =phead;while(pcur){ printf("%d->",pcur->data);pcur =pcur->next;}printf("NULL\n");}voidPushBack(SLTNode**pphead,SLTDataType x){ assert(pphead);SLTNode*newnode =SLTBuyNode(x);if(*pphead ==NULL){ *pphead =newnode;}else{ SLTNode*ptail =*pphead;while(ptail->next){ ptail =ptail->next;}ptail->next =newnode;}}voidPushFront(SLTNode**pphead,SLTDataType x){ assert(pphead);SLTNode*newnode =SLTBuyNode(x);newnode->next =*pphead;*pphead =newnode;}voidPopBack(SLTNode**pphead){ assert(pphead &&*pphead);SLTNode*prev =*pphead;SLTNode*ptail =*pphead;if((*pphead)->next ==NULL){ free(*pphead);*pphead =NULL;}else{ while(ptail->next){ prev =ptail;ptail =ptail->next;}free(ptail);ptail =NULL;prev->next =NULL;}}voidPopFront(SLTNode**pphead){ assert(pphead &&*pphead);SLTNode*next =(*pphead)->next;free(*pphead);*pphead =next;}voidSLTInsert(SLTNode**pphead,SLTNode*pos,SLTDataType x){ assert(pphead &&*pphead);assert(pos);SLTNode*newnode =SLTBuyNode(x);if(pos ==*pphead){ PushFront(pphead,x);}else{ SLTNode*prev =*pphead;while(prev->next !=pos){ prev =prev->next;}newnode->next =pos;prev->next =newnode;}}voidSLTInsertAfter(SLTNode**pphead,SLTNode*pos,SLTDataType x){ assert(pphead &&*pphead);assert(pos);SLTNode*newnode =SLTBuyNode(x);newnode->next =pos->next;pos->next =newnode;}voidSLTErase(SLTNode**pphead,SLTNode*pos){ assert(pphead &&*pphead);assert(pos);if(pos ==*pphead){ PopFront(pos);}else{ SLTNode*prev =*pphead;while(prev->next !=pos){ prev =prev->next;}prev->next =pos->next;free(pos);pos =NULL;}}voidSLTEraseAfter(SLTNode**pphead,SLTNode*pos){ assert(pphead &&*pphead);assert(pos &&pos->next);SLTNode*del =pos->next;pos->next =del->next;free(del);del =NULL;}voidSLTDestroy(SLTNode**pphead){ assert(pphead &&*pphead);SLTNode*pcur =*pphead;while(pcur){ SLTNode*next =pcur->next;free(pcur);pcur =next;}*pphead =NULL;}SLTNode*SLTFind(SLTNode*phead,SLTDataType x){ SLTNode*pcur =phead;while(pcur){ if(pcur->data ==x){ returnpcur;}pcur =pcur->next;}returnNULL;}
7.3 SList_test.c 文件
#include"SList.h"voidTest01(){ SLTNode*plist =NULL;SLTDataType x =1;PushBack(&plist,x);PushBack(&plist,2);PushBack(&plist,3);PushBack(&plist,4);PushBack(&plist,5);SLTPrint(plist);SLTNode*find =SLTFind(plist,3);if(find ==NULL){ printf("没有找到!\n");}else{ printf("找到了!\n");}}intmain(){ Test01();return0;}
运行结果:

全文至此结束!!!
写作不易,不知各位老板能否给个一键三连或是一个免费的赞呢(▽)(▽),这将是对我最大的肯定与支持!!!谢谢!!!(▽)(▽)