本文已收录至:数据结构 | C语言
更多知识尽在此专栏中!



文章目录

  • 前言
  • 正文
    • 链表打印与销毁
      • 打印
      • 销毁
    • 尾部插入与删除
      • 节点申请
      • 尾插
      • 尾删
    • 头部插入与删除
      • 头插
      • 头删
    • 节点查找与修改
      • 查找
      • 修改
    • 任意位置插入与删除
      • 简单版
        • 插入
        • 删除
      • 困难版
        • 插入
        • 删除
    • 源码区
      • 功能声明头文件部分
      • 功能实现源文件部分
      • 主函数调用源文件部分
    • 相关OJ试题推荐
  • 总结

前言

单链表 是一种链式存取的数据结构,用一组地址任意的存储单元存放线性表中的数据元素。链表 中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置) ,元素就是存储数据的存储单元,指针就是连接每个结点的地址数据

这是百度百科对于 单链表 的解释,通俗来说,单链表 就是一种数据结构,它包含了一个 数据域 data 和一个 指针域 next ,最大的特点就是 链式存储 。链表有很多种,其中 单链表 是最基本、最简单的一种结构,很多OJ题都会利用 单链表 出题,后面的部分高阶数据结构也会用到 单链表 ,因此学好 单链表 很重要。除了 单链表 外,还有 双向带头循环链表 (后面会介绍)等链表类型,先来进入 单链表 的学习吧!


正文

链表打印与销毁

打印

单链表 创建时是一个结构体类型的指针,一开始指向空,只有在经过插入数据后才会有自己的指向 ,因此我们可以根据这一特点,遍历 整个 单链表 ,并输出其中的 数据域 data

void SLTPrint(const SLT** pphead)//单链表的打印{assert(pphead);//不需要断言 *pphead ,因为存在空链表打印的情况,是合法的printf("\n\n当前链表为: ");const SLT* cur = *pphead;while (cur){printf("%d->", cur->data);cur = cur->next;//cur要向后移动}printf("NULL\n\n\n");//链表最后为空}

销毁

销毁 单链表 也需要将其 遍历 一遍,因为链表中的元素不是连续存放的,只能逐个节点销毁 ,销毁思想为:利用 *pphead 遍历整个 单链表 ,保存头节点 *pphead 的下一个节点信息至 cur,释放原头节点,更新头节点信息(把 cur 的值赋给头节点 *pphead )如此重复,直到释放完所有节点即可。

//不带哨兵位的单链表不需要初始化void SLTDestroy(SLT** pphead)//单链表的销毁{assert(pphead);//一二级指针都不能为空//空表不走销毁程序,但不能报错if (*pphead){while (*pphead){SLT* cur = (*pphead)->next;//记录头的下一个位置free(*pphead);*pphead = cur;//向后移动,不断销毁}}}

尾部插入与删除

节点申请

单链表 是由一个一个独立存在的节点组成的结构,当涉及插入操作时,向内存申请节点,涉及删除操作时,就要把对应的节点销毁

static SLT* buyNode(const SLTDataType x)//买节点{SLT* newnode = (SLT*)malloc(sizeof(SLT));assert(newnode);//防止申请失败的情况newnode->data = x;newnode->next = NULL;return newnode;//返回买好的节点}

尾插

单链表 尾插是比较费劲的,因为得先通过头节点指针向后 遍历 找到尾节点,然后将尾节点与新节点之间建立链接关系,其中还得分情况尾插

  • 链表为空,直接把新节点赋给头节点
  • 不为空,就需要找到尾节点,建立链接关系

关于 单链表 中函数用二级指针的问题:
插入或删除时,如果是第一次操作,需要对头节点本身造成改变,且头节点是一个 一级指针 ,因此需要通过 二级指针 的方式来在函数中改变头节点的值。至于后续的操作,都只是改变了结构体中的 next 值,因此使用 一级指针 就够了,但是为了函数设计时的普适性,单链表 中的函数参数都设计成了 二级指针 的形式。

void SLTPushBack(SLT** pphead, const SLTDataType x)//尾插{assert(pphead);SLT* newnode = buyNode(x);SLT* tail = *pphead;//尾插分情况,链表为空,直接赋值,不为空,找到尾,再链接if (tail == NULL){*pphead = tail = newnode;//直接赋值}else{while (tail->next != NULL){tail = tail->next;//找到尾节点}tail->next = newnode;//链接}//SLTInsert(pphead, NULL, x);//可以复用任意位置前插}

尾删

尾删操作与尾插基本一致,同样是需要找到尾节点,不过每次 tail 指针在向后移动前,会先使用一个 prev 指针保存 tail 的信息,当 tail 为尾节点时,释放 tail ,并将 prev->next 置空,此时的 prev 就是新的尾节点,因为原理都差不多,这里就不用动图展示了,值得注意的是尾删也分两种情况

  • 只有一个节点,此时直接释放头节点(尾节点),链表置空
  • 存在多个节点,需要先找到尾节点与尾节点的上一个节点,确定新的尾
  • 并不是所有情况都能删除,比如表空的情况,是不能执行操作的,可以用断言处理


void SLTPopBack(SLT** pphead)//尾删{assert(pphead);assert(*pphead);//如果链表为空,是删不了的SLT* tail = *pphead;SLT* prev = NULL;while (tail->next){prev = tail;//保存上一个节点信息tail = tail->next;//找到尾节点}free(tail);//分情况,如果prev是空,说明删除的尾节点同时也是头节点if (prev)prev->next = tail = NULL;//把尾节点的上一个节点指向空else*pphead = NULL;//此时直接把prev置空/*SLT* tail = *pphead;while (tail->next){tail = tail->next;}SLTErase(pphead, tail);*///可以复用任意位置删除}

头部插入与删除

头插

对于头部操作来说,单链表 是很轻松的,比如 单链表 头插的本质就是将 新节点 newnode头节点 *pphead 链接,然后更新头节点信息就行了,即 *pphead = newnode ,三行代码就解决了。

void SLTPushFront(SLT** pphead, const SLTDataType x)//头插{assert(pphead);SLT* newhead = buyNode(x);newhead->next = *pphead;//直接链接*pphead = newhead;//更新头节点信息//代码简洁之道//SLTInsert(pphead, *pphead, x);//可以复用任意位置前插}

头删

头删也是比较简单的,先用 cur 指向头节点,先保存 头节点 cur 的下一个节点信息至 节点 newhead,释放 原头节点 cur,更新 newhead 为链表的新头,即 *pphead = newnode 当然涉及删除的操作,都需要进行表空检查,如果链表为空,是不能执行删除的。

void SLTPopFront(SLT** pphead)//头删{assert(pphead);assert(*pphead);//头删同样存在空不能删的情况//先找到头的下一个节点,然后赋值新头SLT* cur = *pphead;//指向头节点SLT* newhead = cur->next;//保存头节点的下一个节点信息free(cur);cur = NULL;*pphead = newhead;//赋值新头//SLTErase(pphead, *pphead);//可以复用任意位置删除}

节点查找与修改

查找

查找函数很简单,遍历+比较 就行了,找到目标元素值后,返回相关节点信息,其实查找这个函数主要是为了配合后面任意插入删除函数的 ,所以比较简单。

SLT* SLTFind(const SLT** pphead, const SLTDataType x)//查找值为x的节点(第一次出现){assert(pphead);SLT* cur = (SLT*)*pphead;//指向头节点while (cur){if (cur->data == x)return cur;//找到了,返回相关节点信息cur = cur->next;}return NULL;//没有找到的情况}

修改

修改函数是在查找函数基础上进行的:当我们输入元素值交给查找函数,找到目标节点后返回其节点信息,然后直接通过返回的节点改变 data 值就行了,在调用查找函数的前提下,一行代码就解决了。

void SLTModify(SLT* node, const SLTDataType val)//修改 node 节点处的值{//注意:在调用函数时,可以通过链式访问,将查找函数的返回值作为形参一传入就行了assert(node);//断言,如果节点node是空指针,是不能做修改的node->data = val;}

任意位置插入与删除

简单版

简单版是在任意位置后插入元素,删除任意位置后的节点,根据 单链表 的特性,对后面节点进行操作是比较简单,无非就是改变链接关系。但是这种对后操作存在缺陷:不适合实现头插、头删

插入

插入(后插)主要分两步

  • 获取信息
  • 改变链接关系

获取信息:有三个关键信息:被插入节点 cur、待插入节点 newnodecur 的下一个节点 tail

改变链接关系:很简单,cur->next = newnode,后插嘛,先把待插入节点链接到 cur 后面,然后再把 newnodetail 链接起来就行了

这里的 nodeAfter 是需要通过查找函数找出来的,它是一个 一级指针

//这两个有缺陷,不能对头节点进行操作,但实现起来比较简单void SLTInsertAfter(SLT* nodeAfter, const SLTDataType x)//任意插,后插法{assert(nodeAfter);SLT* cur = nodeAfter;SLT* newnode = buyNode(x);SLT* tail = cur->next;//先保存被插入节点的下一个节点信息//更改链接关系,后插完成cur->next = newnode;newnode->next = tail;}
删除

删除思路,和头删差不多

  • 找到待插入节点 cur
  • 找到 cur 的下一个节点 tail
  • 找到 tail 的下一个节点 newtail

接下来就很简单了,释放 tail 节点,更改链接关系,当然断言是少不了

void SLTEraseAfter(SLT* nodeAfter)//任意删,删除后面节点{assert(nodeAfter);assert(nodeAfter->next);//如果目标节点的下一个为空,是后删不了的SLT* cur = nodeAfter;SLT* tail = cur->next;//待删除的节点SLT* newtail = tail->next;//新尾free(tail);tail = NULL;cur->next = newtail;//更改链接关系}

困难版

困难版就比较麻烦了,因为它要实现的是任意位置前插元素、删除任意位置的节点,单链表 的最大缺点是不能回退,这就意味着即使我们得到了待删除节点,也是很难求出它的上一个节点的 ,对于这种尴尬情况,只能老老实实从头节点处开始向后 遍历 寻找,直到找到待删除节点的上一个节点。

插入

其实这个也没有多困难,无非就是比上一种多个参数,然后多了一步遍历操作而已,内核思想任然不变

  • 获取信息
  • 更改链接关系

//这两个实现起来比较麻烦,但是很全能void SLTInsert(SLT** pphead, SLT* node, const SLTDataType x)//任意插,前插法{assert(pphead);SLT* newnode = buyNode(x);SLT* cur = *pphead;SLT* prev = NULL;while (cur != node){prev = cur;//要找到目标节点的上一个节点cur = cur->next;}//判断一下,是否为空表插入//走的是尾插的那一套思想if (prev){prev->next = newnode;newnode->next = node;}else{newnode->next = node;*pphead = newnode;//空表需要更新头节点信息}}
删除

删除是一样的逻辑,不过多了一个 tail 而已,指向的位置是 node 的下一个节点,其余步骤跟插入基本一致,之后也是一样的更改链接关系,一样需要判断是否为空表,如果为空表需要更新头节点信息

其实现在看来,困难版的插入删除,就像是尾部插入删除的升级版,有些麻烦,但很可靠,困难版的任意位置插入删除可以完全替代头尾插入删除,也就是说写这一对函数就够了。

void SLTErase(SLT** pphead, SLT* node)//任意删,删除当前节点{assert(pphead);assert(node);//不必检查*pphead的合法性,查验node就行了SLT* cur = *pphead;//走的是尾删的那一套思想SLT* prev = NULL;SLT* tail = node->next;while (cur != node){prev = cur;cur = cur->next;}free(node);//跟尾插一样,需要判断一下if (prev)prev->next = tail;else*pphead = tail;}

源码区

代码放一起看,会更好理解一些~

功能声明头文件部分

#pragma once#include#include#include#includetypedef int SLTDataType;//单链表的数据类型typedef struct SListNode{SLTDataType data;//数据域struct SListNode* next;//指针域}SLT;//重命名为 SLTvoid SLTDestroy(SLT** pphead);//单链表的销毁void SLTPrint(const SLT** pphead);//单链表的打印void SLTPushBack(SLT** pphead, const SLTDataType x);//尾插void SLTPopBack(SLT** pphead);//尾删void SLTPushFront(SLT** pphead, const SLTDataType x);//头插void SLTPopFront(SLT** pphead);//头删//这两个有缺陷,不能对头节点进行操作,但实现起来比较简单void SLTInsertAfter(SLT* nodeAfter, const SLTDataType x);//任意插,后插法void SLTEraseAfter(SLT* nodeAfter);//任意删,删除后面节点//这两个实现起来比较麻烦,但是很全能void SLTInsert(SLT** pphead, SLT* node, const SLTDataType x);//任意插,前插法void SLTErase(SLT** pphead, SLT* node);//任意删,删除当前节点SLT* SLTFind(const SLT** pphead, const SLTDataType x);//查找值为x的节点(第一次出现)void SLTModify(SLT* node, const SLTDataType val);//修改 node 节点处的值

功能实现源文件部分

#define _CRT_SECURE_NO_WARNINGS 1#include"SList.h"//不带哨兵位的单链表不需要初始化void SLTDestroy(SLT** pphead)//单链表的销毁{assert(pphead);//一二级指针都不能为空//空表不走销毁程序,但不能报错if (*pphead){while (*pphead){SLT* cur = (*pphead)->next;//记录头的下一个位置free(*pphead);*pphead = cur;//向后移动,不断销毁}}}void SLTPrint(const SLT** pphead)//单链表的打印{assert(pphead);//不需要断言 *pphead ,因为存在空链表打印的情况,是合法的printf("\n\n当前链表为: ");const SLT* cur = *pphead;while (cur){printf("%d->", cur->data);cur = cur->next;//cur要向后移动}printf("NULL\n\n\n");//链表最后为空}static SLT* buyNode(const SLTDataType x)//买节点{SLT* newnode = (SLT*)malloc(sizeof(SLT));assert(newnode);//防止申请失败的情况newnode->data = x;newnode->next = NULL;return newnode;//返回买好的节点}void SLTPushBack(SLT** pphead, const SLTDataType x)//尾插{assert(pphead);SLT* newnode = buyNode(x);SLT* tail = *pphead;//尾插分情况,链表为空,直接赋值,不为空,找到尾,再链接if (tail == NULL){*pphead = tail = newnode;//直接赋值}else{while (tail->next != NULL){tail = tail->next;//找到尾节点}tail->next = newnode;//链接}//SLTInsert(pphead, NULL, x);//可以复用任意位置前插}void SLTPopBack(SLT** pphead)//尾删{assert(pphead);assert(*pphead);//如果链表为空,是删不了的SLT* tail = *pphead;SLT* prev = NULL;while (tail->next){prev = tail;//保存上一个节点信息tail = tail->next;//找到尾节点}free(tail);//分情况,如果prev是空,说明删除的尾节点同时也是头节点if (prev)prev->next = tail = NULL;//把尾节点的上一个节点指向空else*pphead = NULL;//此时直接把prev置空/*SLT* tail = *pphead;while (tail->next){tail = tail->next;}SLTErase(pphead, tail);*///可以复用任意位置删除}void SLTPushFront(SLT** pphead, const SLTDataType x)//头插{assert(pphead);SLT* newhead = buyNode(x);newhead->next = *pphead;//直接链接*pphead = newhead;//更新头节点信息//代码简洁之道//SLTInsert(pphead, *pphead, x);//可以复用任意位置前插}void SLTPopFront(SLT** pphead)//头删{assert(pphead);assert(*pphead);//头删同样存在空不能删的情况//先找到头的下一个节点,然后赋值新头SLT* cur = *pphead;//指向头节点SLT* newhead = cur->next;//保存头节点的下一个节点信息free(cur);cur = NULL;*pphead = newhead;//赋值新头//SLTErase(pphead, *pphead);//可以复用任意位置删除}//这两个有缺陷,不能对头节点进行操作,但实现起来比较简单void SLTInsertAfter(SLT* nodeAfter, const SLTDataType x)//任意插,后插法{assert(nodeAfter);SLT* cur = nodeAfter;SLT* newnode = buyNode(x);SLT* tail = cur->next;//先保存被插入节点的下一个节点信息//更改链接关系,后插完成cur->next = newnode;newnode->next = tail;}void SLTEraseAfter(SLT* nodeAfter)//任意删,删除后面节点{assert(nodeAfter);assert(nodeAfter->next);//如果目标节点的下一个为空,是后删不了的SLT* cur = nodeAfter;SLT* tail = cur->next;//待删除的节点SLT* newtail = tail->next;//新尾free(tail);tail = NULL;cur->next = newtail;//更改链接关系}//这两个实现起来比较麻烦,但是很全能void SLTInsert(SLT** pphead, SLT* node, const SLTDataType x)//任意插,前插法{assert(pphead);SLT* newnode = buyNode(x);SLT* cur = *pphead;SLT* prev = NULL;while (cur != node){prev = cur;//要找到目标节点的上一个节点cur = cur->next;}//判断一下,是否为空表插入//走的是尾插的那一套思想if (prev){prev->next = newnode;newnode->next = node;}else{newnode->next = node;*pphead = newnode;//空表需要更新头节点信息}}void SLTErase(SLT** pphead, SLT* node)//任意删,删除当前节点{assert(pphead);assert(node);//不必检查*pphead的合法性,查验node就行了SLT* cur = *pphead;//走的是尾删的那一套思想SLT* prev = NULL;SLT* tail = node->next;while (cur != node){prev = cur;cur = cur->next;}free(node);//跟尾插一样,需要判断一下if (prev)prev->next = tail;else*pphead = tail;}SLT* SLTFind(const SLT** pphead, const SLTDataType x)//查找值为x的节点(第一次出现){assert(pphead);SLT* cur = (SLT*)*pphead;//指向头节点while (cur){if (cur->data == x)return cur;//找到了,返回相关节点信息cur = cur->next;}return NULL;//没有找到的情况}void SLTModify(SLT* node, const SLTDataType val)//修改 node 节点处的值{//注意:在调用函数时,可以通过链式访问,将查找函数的返回值作为形参一传入就行了assert(node);//断言,如果节点node是空指针,是不能做修改的node->data = val;}

主函数调用源文件部分

#define _CRT_SECURE_NO_WARNINGS 1#include"SList.h"void menu(){printf("0.退出1.打印\n");printf("2.尾插3.尾删\n");printf("4.头插5.头删\n");printf("6.任意插(后插)7.任意删(后删)\n");printf("8.任意插(前插)9.任意删(当前)\n");printf("10.查找11.修改\n");}int main(){SLT* s = NULL;int input = 1;while (input){menu();printf("请选择:>");scanf("%d", &input);system("cls");//清屏函数,让显示效果更好int val = 0;//待插入值int pos = 0;//待查找节点值switch (input){case 0:printf("成功退出\n");break;case 1:SLTPrint(&s);break;case 2:printf("请输入一个数:>");scanf("%d", &val);SLTPushBack(&s, val);break;case 3:SLTPopBack(&s);break;case 4:printf("请输入一个数:>");scanf("%d", &val);SLTPushFront(&s, val);break;case 5:SLTPopFront(&s);break;case 6:printf("请输入被插入的节点值:>");scanf("%d", &pos);printf("请输入一个数:>");scanf("%d", &val);SLTInsertAfter(SLTFind(&s, pos), val);break;case 7:printf("请输入被删除的节点值:>");scanf("%d", &pos);SLTEraseAfter(SLTFind(&s, pos));break;case 8:printf("请输入被插入的节点值:>");scanf("%d", &pos);printf("请输入一个数:>");scanf("%d", &val);SLTInsert(&s, SLTFind(&s, pos), val);break;case 9:printf("请输入被删除的节点值:>");scanf("%d", &pos);SLTErase(&s, SLTFind(&s, pos));break;case 10:printf("请输入被查找的节点值:>");scanf("%d", &pos);SLT* tmp = SLTFind(&s, pos);printf("当前节点的地址为:%p\n", tmp);break;case 11:printf("请输入被修改的节点值:>");scanf("%d", &pos);printf("请输入一个数:>");scanf("%d", &val);SLTModify(SLTFind(&s, pos), val);break;default :printf("选择错误,重新选择!\n");break;}}SLTDestroy(&s);//每次程序运行结束,都会执行销毁函数return 0;}

相关OJ试题推荐

下面是几道关于 单链表 的OJ试题,可以试着解决一下,加强对链表的认识

  1. 203.移除链表元素
  2. 206.反转单链表
  3. 876.链表的中间结点
  4. 链表中倒数第k个结点
  5. 21.合并两个有序链表
  6. CM11 链表分割
  7. OR36 链表的回文结构
  8. 160.相交链表
  9. 141.环形链表
  10. 141.环形链表 ||
  11. 138.复制带随机指针的链表

总结

以上就是关于 单链表 的全部内容了,单链表 中用到了 二级指针 这个东西,如果使用带哨兵位的 单链表 就可以不用 二级指针 ,但是这种结构用的比较少,单纯的学好 单链表 ,能快速提高我们对链表的认识,毕竟链表这个工具后续还会用到。从文中可以看出,单链表 相对于 顺序表 ,不用考虑空间问题,且头插头删效率很高,可惜 单链表 不支持下标的随机访问。总之,顺序表单链表 各有各的用途,二者相辅相成,都是很不错的数据结构。

如果你觉得本文写的还不错的话,期待留下一个小小的赞,你的支持是我分享的最大动力!

如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正

相关文章推荐
数据结构 | 顺序表
数据结构 | 时间复杂度与空间复杂度
C语言进阶——动态内存管理