目录
一、链表的概念及结构
1.1 链表的独特定义
1.2 火车车厢式的形象类比
1.3 节点的结构体定义剖析
1.4 链表物理与逻辑结构的特性差异
二、单链表的实现
2.1 类型定义的优化策略
2.2 链表操作函数的声明框架
2.3 链表操作函数的实现细节
三、链表的分类
前言
在程序世界里,数据结构如同建筑基石,支撑起各种复杂的软件系统。其中,单链表作为一种基础且独特的线性数据结构,以其灵活的存储方式和多样的操作方法,在算法设计、系统开发等领域发挥着不可替代的作用。今天,让我们一同深入探索单链表的奇妙世界,揭开它神秘的面纱
一、链表的概念及结构
1.1 链表的独特定义
链表是一种在物理存储上非连续、非顺序的存储结构,它打破了传统顺序存储的束缚。在链表中,数据元素的逻辑顺序并非依赖于物理存储位置的相邻性,而是通过指针的巧妙链接来实现。这就好比在一个大型社区中,各个住户的房子并不一定紧密相连,但每家都有一张写着下一户地址的纸条,凭借这些纸条,我们就能按照特定顺序 “走访” 所有住户,这里的纸条就如同链表中的指针,引导我们在数据的 “社区” 中穿梭
1.2 火车车厢式的形象类比
为了更直观地理解链表结构,我们不妨将其类比为火车车厢。在不同的运营时期,火车会根据客流量调整车厢数量,淡季减少车厢,旺季增加车厢,且每节车厢都相互独立,增减某节车厢不会干扰其他车厢的存在。想象一下,每节车厢的门都锁着,且需要特定的钥匙才能打开,而我们一次只能携带一把钥匙。要从车头顺利走到车尾,最好的办法就是在每节车厢里放置下一节车厢的钥匙
在链表的世界里,这些 “车厢” 就是一个个 “结点 / 节点”。与火车车厢类似,链表节点也是独立的个体,只不过它们存在于计算机的内存空间中,通过申请内存来创建。每个节点主要包含两部分内容:一是当前节点要存储的数据,这就像车厢里装载的货物;二是用于保存下一个节点地址的指针变量,它如同车厢里放置的下一节车厢的钥匙,凭借这个 “钥匙”,我们可以从当前节点顺利找到下一个节点,实现对链表的遍历
1.3 节点的结构体定义剖析
在 C 语言中,我们可以借助结构体来定义链表节点。假设当前节点保存的是整型数据,其结构体定义如下:
当我们要保存一个整型数据时,实际上是向操作系统申请了一块内存空间。这块内存不仅要存放整型数据,还要预留空间来保存下一个节点的地址。如果当前节点是链表的最后一个节点,那么保存的地址将为空,就如同最后一节车厢没有下一节车厢的钥匙一样
1.4 链表物理与逻辑结构的特性差异
链表在逻辑上呈现出连续有序的结构,通过指针将各个节点依次连接,形成一个有序序列,从第一个节点出发,沿着指针就能按顺序访问到每一个节点。然而在物理层面,链表中的节点在内存中并不一定连续存储。这是因为节点通常是从堆上申请的空间,堆内存的分配遵循特定策略,每次申请的空间可能连续,也可能分散在不同的内存区域。这种特性赋予了链表在内存管理上的高度灵活性,但也增加了对其理解和操作的难度
二、单链表的实现
2.1 类型定义的优化策略
为了提升代码的可读性和可维护性,我们常常运用typedef
对数据类型进行重定义。在单链表的实现过程中,定义SLTDataType
来代表节点中存储的数据类型,并使用typedef
将SLTNode
作为链表节点的类型别名。这样一来,在后续编写代码时,使用这些类型会更加便捷
2.2 链表操作函数的声明框架
单链表的常见操作涵盖打印链表、插入节点(包括头部插入、尾部插入、指定位置插入)、删除节点(头部删除、尾部删除、指定位置删除)、查找节点以及销毁链表等。下面是这些操作函数的声明:
具体细节我会在下面进行详细讲解!!!
2.3 链表操作函数的实现细节
打印链表(SLTPrint):打印链表的过程,本质上是从链表的头节点开始,逐个访问每个节点,并输出节点中的数据。当遇到NULL
指针时,意味着已经到达链表末尾,打印操作结束
尾部插入(SLTPushBack):在链表尾部插入新节点时,需要分情况处理。如果链表为空,直接创建新节点并让头指针指向它;若链表不为空,则遍历链表找到最后一个节点,然后在其后面插入新节点
这块存在一个问题,为什么要传二级指针,因为在链表为空时,我们需要改变头指针的指向,所以我们要传地址,一级指针的地址要用二级指针来接受
由于进行插入操作都要申请新节点,所以我们可以分装一个申请新节点的接口,后面使用直接调用就好了
新节点申请(BuyNewNode):
头部插入(SLTPushFront):头部插入新节点相对简单,先创建新节点,将新节点的next
指针指向原头节点,再把新节点设为头节点即可
这个传二级指针和尾插又有一点不一样,每次插入都要改变头指针的指向,尾插只是在链表为空的时候才要改变头指针的指向
尾部删除(SLTPopBack):进行尾部删除操作时,要考虑链表为空和只有一个节点的特殊情况。链表为空时直接返回;若只有一个节点,释放该节点并将头指针置为NULL
;若有多个节点,则遍历链表找到倒数第二个节点,释放最后一个节点,并把倒数第二个节点的next
指针置为NULL
为啥传二级指针,同理,删除最后一个节点时,需要改变头指针的指向
头部删除(SLTPopFront):头部删除操作只需保存原头节点的下一个节点,释放原头节点,然后将头指针更新为保存的下一个节点
为啥传二级指针,同理
查找(SLTFind):查找操作从链表的头节点开始,逐个比较节点的数据与目标数据。一旦找到匹配的数据,就返回该节点的指针;若遍历完整个链表都未找到,则返回NULL
指定位置之前插入(SLTInsert):在指定位置之前插入新节点时,先判断是否在头节点之前插入。若是,直接采用头部插入的逻辑;若不是,则遍历链表找到指定位置的前一个节点,然后插入新节点
删除指定位置节点(SLTErase):删除指定位置的节点时,同样先判断是否为头节点。如果是,使用头部删除逻辑;否则,找到前一个节点,调整指针关系并释放要删除的节点
有人可能想问为啥没有将pos置为空,因为pos是用一级指针接收的,想要改变pos得传二级指针,所以置为空也没用,但是我们在调用该函数后可以手动将pos置为空
至于 为啥传二级指针,我就不一一解释了,都和尾插 头插一个原理,你记住,想要通过形参的改变改变实参,就传地址,假如你本身是个指针,那你想改变这个指针就得传指针的地址,也是二级指针
指定位置之后插入(SLTInsertAfter):在指定位置之后插入新节点,只需创建新节点,将新节点的next
指针指向指定位置节点的下一个节点,再把指定位置节点的next
指针指向新节点
删除指定位置之后的节点(SLTEraseAfter):删除指定位置之后的节点,先保存要删除节点的下一个节点,释放要删除的节点,然后调整指针,让指定位置节点的next
指针指向保存的下一个节点
销毁链表(SListDesTroy):销毁链表时,需要从链表头部开始,逐个释放每个节点的内存空间,避免内存泄漏
三、链表的分类
链表的结构丰富多样,根据是否带头节点、是否双向链接以及是否循环这三个维度进行组合,总共可以形成 8 种不同的链表结构。在实际应用中,最常用的是以下两种:
- 无头单向非循环链表:这种链表结构相对简单,通常不会单独用于存储数据。在实际项目里,它更多地作为其他复杂数据结构的子结构,例如哈希桶、图的邻接表等。此外,无头单向非循环链表在笔试面试中频繁出现,是考查数据结构知识的重要内容。
- 带头双向循环链表:虽然带头双向循环链表的结构最为复杂,但在单独存储数据时却具有显著优势。在实际应用的链表数据结构中,它的使用频率很高。尽管其结构复杂,但通过代码实现后会发现,这种结构带来了诸多便利,例如在插入和删除操作时,无需像无头单向非循环链表那样对边界情况进行大量特殊处理,实现起来反而更加简洁高效。
单链表作为数据结构领域的重要基础,不仅在理论研究中占据关键地位,在实际编程应用中也有着广泛的用途。从操作系统的内存管理到数据库索引的构建,从编译器的语法分析到游戏开发中的资源管理,单链表都发挥着不可或缺的作用。希望通过本文的详细介绍,读者能对单链表有更深入的理解和掌握,在未来的编程实践中灵活运用这一强大的数据结构工具,解决各种复杂的问题