当前位置: 首页 > news >正文

深入解析数据结构之单链表

目录

  • 一、单链表
    • 1.1 概念与结构
      • 1.1.1 节点/结点
      • 1.1.2 链表的性质
      • 1.1.3 链表的打印
    • 1.2 单链表的实现
      • 1.2.1 尾插
      • 1.2.2 头插
      • 1.2.3 尾删
      • 1.2.4 头删
      • 1.2.5 查找
      • 1.2.6 在指定位置之前插入数据
      • 1.2.7 在指定位置之后插入数据
      • 1.2.8 删除pos节点
      • 1.2.9 删除pos之后的节点
      • 1.2.10 销毁链表
  • 总结


前言
在上一篇文章中深入解析数据结构之顺序表中
顺序表的特点有以下几点:

  1. 中间/头部的插入删除,时间复杂度为O(N)
  2. 增容需要申请新空间,拷贝数据,释放旧空间。这其中有不小的消耗
  3. 增容一般是呈2倍的增长,势必会有一定空间的浪费。例如当前容量为100,满了以后增容到200,再继续插入5个数据,后面没有数据插入了,就浪费95个数据空间。

接下来写的该数据结构就可以很好的解决这些缺陷了:

  • 其头部插入删除,时间复杂度为O(1)
  • 不需要增容
  • 不存在空间浪费

一、单链表

1.1 概念与结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
即其逻辑结构:是线性的
物理结构:不一定是线性的

在这里插入图片描述
在我们日常生活中的火车在淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只需要将火车里的某节车厢去掉/加上,不会影响其他车厢,每节车厢都是独立存在的。

1.1.1 节点/结点

在链表中,也存在这样的“车厢”
在这里插入图片描述
内存中的图示大概如下:
在这里插入图片描述

与顺序表不同的是,链表中每节车厢都是独立申请下来的空间,称为节点/结点

节点的组成有两个部分:当前节点要保存的数据和保存下一个节点的地址(指针变量)

图中指针变量plist保存的是第一个节点的地址,称plist此时指向第一个节点,如果希望plist指向第二个节点,只要修改plist保存的地址内容即可

//空节点
int* plist = NULL;

链表中每个节点都是独立申请的(即需要插入数据时才去申请一块节点的空间),需要通过指针变量来保存下一个节点位置才能从当前节点找到下一个节点

1.1.2 链表的性质

  1. 链式结构在逻辑上时连续的,在物理上不一定连续
  2. 节点一般是从堆上申请的
  3. 从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能连续,可能不连续

这里定义一下链表的结构,由于链表是由一个个节点组成,所以定义链表的结构即定义节点的结构

SList.h

在这里插入图片描述

1.1.3 链表的打印

函数声明:

//打印单链表
void SLTPrint(SLTNode* phead);

SList.c

在这里插入图片描述

test.c

在这里插入图片描述
在这里插入图片描述

1.2 单链表的实现

1.2.1 尾插

尾插操作分两步

  1. 申请一个新节点把数据放入
  2. 将原来的尾节点和新申请的节点连接起来

函数声明:

//尾插
void SLTPushBack(SLTNode* phead, SLTDataType x);

因为插入操作首先都要申请一个节点把数据放进去,所以这里单独设计一个函数来完成该操作,该函数要返回一个指向新节点的指针
在这里插入图片描述

SList.c

在这里插入图片描述

test.c

在这里插入图片描述
但是这里问题来了,可以看出,并不是所预测的1 -> 2 -> 3 -> 4 -> NULL,这里调试一下
在这里插入图片描述
可以发现形参的改变没有影响实参,是传值调用。然而有兄弟就说了这里plist指针里存的是地址呀,怎么会是传值呢
在这里插入图片描述
如图有两个变量a和pa,pa变量是一个指针,存放的是a变量的地址,而pa也有自己的地址,pa变量的创建也是向内存申请一块空间,这块空间有自己的地址,这里指针变量存的是a的地址,而不是自己的地址。

这里要形参的改变影响实参,应该把plist的地址传过去,而不是把其保存的地址传过去
在这里插入图片描述
传一级指针的地址,形参要用二级指针接收,更改后代码如下:

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);

在这里插入图片描述
注意:pphead是一级指针的地址,对其解引用一次就是一级指针,也就是plist,ptail是一级指针,其指向* pphead

补充一下:pphead是一级指针的地址,如果pphead为空,一级指针都无法解引用,而* pphead是指向第一个节点的地址,第一个节点可以为空,所以断言的时候只要pphead不能为空就可

1.2.2 头插

在这里插入图片描述
补充:这里phead就是* pphead

函数声明:

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);

pphead始终指向第一个节点,头部插入节点,第一个节点始终会变为新的节点,因为头节点会发生改变,所以这里使用二级指针

函数定义:
在这里插入图片描述

测试函数:
在这里插入图片描述

1.2.3 尾删

在尾删中,最后一个节点(此时也是头节点)也能被删除,也就是头节点变为空,头节点会发生改变,函数声明时候也就是** pphead
函数声明

//尾删
void SLTPopBack(SLTNode** pphead);

在这里插入图片描述
函数定义
在这里插入图片描述
这串代码看似是没有问题的,但是运行之后却有问题
在这里插入图片描述
这里返回码不仅为负数,第四次删除的结果应该为NULL才对
这就是因为在只有一个节点的情况下,prev->next是对空指针进行解引用了。这种情况下直接讲头节点删除就为空了,不需要再找尾节点的前一个节点了

改正后代码如下:
在这里插入图片描述
在这里插入图片描述
注意:在链表中每个节点都是独立的,可以单独申请或释放一个节点

1.2.4 头删

在这里插入图片描述
函数声明:头删即头节点不断地在改变,这里依旧使用二级指针

//头删
void SLTPopFront(SLTNode** pphead);

在这里插入图片描述
在这里插入图片描述
简单总结一下:

单链表与顺序表的特点相反,链表中尾部的插入删除时间复杂度为O(N),头部的插入删除时间复杂度为O(1),所以没有完美的数据结构,其具体的使用要根据当下的场景

1.2.5 查找

函数声明:

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);

函数定义:
在这里插入图片描述

没有找到返回NULL,找到了返回一个指向当前节点的指针

测试函数:
在这里插入图片描述

1.2.6 在指定位置之前插入数据

函数声明:

//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);

因为有可能在头节点之前插入数据,所以这里函数声明中依旧使用二级指针,这样才能通过传址操作使得头节点的操作在函数外部生效
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
看似没有问题,然而出现了报错,这是因为若pos为第一个节点,此时prev也是第一个节点
在这里插入图片描述
在这里插入图片描述
持续循环,此时prev为空了,再向下推在while()判断条件里,就是对空指针进行解引用了

若pos为第一个节点,就不需要找pos的前一个节点,而是在头节点的前面直接插入一个新的节点
在这里插入图片描述
在这里插入图片描述

1.2.7 在指定位置之后插入数据

函数声明:

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);

在这里插入图片描述

注意,这里如果要在3和4之间插入一个新的节点,不可以从前向后依次连

pos->next = newnode;
newnode->next = pos->next;//pos->next已修改,找不到4的节点了

所以先连newnode和4,再连3和newnode
在这里插入图片描述
在这里插入图片描述

1.2.8 删除pos节点

函数声明:

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);

基于上面代码就可以发现链表的特点就是pos前的节点无法直接找到,要通过头节点遍历寻找,pos之后的节点很好找
在这里插入图片描述
注意:这里不能先释放3这个节点,否则无法通过pos找4这个节点,此时pos就是野指针了,不能对野指针解引用找下一个节点
在这里插入图片描述
在这里插入图片描述
可以看出这里的返回码依旧有问题
参考前面例子,pos为头节点需要特殊处理
在这里插入图片描述
在这里插入图片描述

1.2.9 删除pos之后的节点

函数声明:

//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);

在这里插入图片描述
这里依旧是先将2和4牵起来,再将3释放掉,不可以先释放3
在这里插入图片描述
在这里插入图片描述

1.2.10 销毁链表

函数声明:

//销毁链表
void SListDestroy(SLTNode** pphead);

因为销毁链表需要把所有的节点销毁掉,包括头节点,所以依旧传二级指针

在这里插入图片描述
这里用next存要销毁节点的下一个节点,当pcur的值不为空,就销毁,接下来pcur走到2这个节点,此节点不为空,在释放第二个节点之前,让next走向3这个节点,释放完成,pcur指向下一个节点,以此类推
在这里插入图片描述
最后pcur为空,循环结束
在这里插入图片描述
销毁前调试结果如预期所想
在这里插入图片描述
继续调试下去销毁操作也是没有问题的,我这里就不展示了
循环结束后,* pphead始终保存第一个节点的地址,但是前面的空间已经还给操作系统了,所以此时* pphead是个野指针,最后将其置为NULL

这里的pcur和next没有置为空的原因是next是在while循环内部定义的变量,跳出循环后就失效了,pcur则是在函数内部,同理,置不置为空都可以,如果以养成好习惯为目的,我还是建议置为空的。


总结

以上就是数据结构之单链表的全部内容了,喜欢的兄弟们不要忘记一键三连给予支持哦~,也是盼着盼着终于等到开学了,主播在家自律性真是太差了!

http://www.dtcms.com/a/359761.html

相关文章:

  • 人工智能加速漏洞利用,15分钟即可完成概念验证?
  • 网络:相比于HTTP,HTTPS协议到底安全在哪?
  • go 开发环境配置 air + dlv debug 踩坑之旅
  • AI基础学习周报十一
  • 大模型——利用RAG构建智能问答平台实战
  • 图像描述编辑器 (Image Caption Editor)
  • 文字的力量:Qwen-Image如何让AI真正“读懂”中文之美
  • HTTPS -> HTTP 引起的 307 状态码与HSTS
  • ans.1中的对象标识符OBJECT_IDENTIFIER----OID
  • 【开题答辩全过程】以 基于springboot的垃圾分类管理系统为例,包含答辩的问题和答案
  • 力扣热题100:合并区间详解(Java实现)(56)
  • 历史数据分析——寒武纪
  • Android开发-活动页面
  • 20.28 《4bit量化模型预处理揭秘:如何节省75%显存高效微调LLM?》
  • leetcode-hot-100(堆)
  • 金融学-货币理论
  • Kafka应用过程中的高频问题
  • 【Linux基础】深入理解计算机存储:GPT分区表详解
  • 对于牛客网—语言学习篇—编程初学者入门训练—复合类型:BC136 KiKi判断上三角矩阵及BC139 矩阵交换题目的解析
  • uvm验证环境中struct(结构体)和class的区别与联系
  • 使用AdaLoRA 自适应权重矩阵微调大模型介绍篇
  • 接口测试总结-含接口测试和前端测试的区别与比较
  • PyTorch 张量(Tensor)详解:从基础到实战
  • 1.9 初始Memory Profiler Package
  • 面试 八股文 经典题目 - HTTPS部分(一)
  • Qt组件布局的经验
  • 深度学习数据加载实战:从 PyTorch Dataset 到食品图像分类全流程解析
  • 实现需求精准预测、运输路径优化及库存高效管理的智慧物流开源了
  • 利用 Java 爬虫获取淘宝拍立淘 API 接口数据的实战指南
  • 图片格式转换v2_tif转png tif转jpg png转tif