欢迎阅读本篇学习笔记。
本篇作为个人计算机专业的学习记录,本文将系统梳理双向链表的相关知识点,从基础概念到代码实现逐步展开,便于后续的复习巩固。如有不足,欢迎大家在评论区交流指正,感谢大家的阅读与支持!
链表的分类
链表的结构非常多样,以下情况组合起来就有八种(2x2x2)链表结构。

带头链表与不带头链表
接下来我先来解释一下何为带头链表与不带头链表。
带头:指的是链表中含有哨兵位节点,该哨兵位即为头节点。讲到这里,回顾一下上篇文章讲到的单链表,或许有人会说原来单链表是带头链表呀,其实不然,单链表是不带头链表。前面我们讲到关于链表的数据插入,尾插与头插等等,在实现这样的操作时,都需要判断一下头节点是否为空。这里所提到的头节点实际上是指链表中第一个有效节点,为了好理解才这样称呼,实际在链表中,头节点指的是哨兵位。带头链表如下图:

带头链表与不带头链表

单向链表与双向链表
顺着刚才的逻辑,我们再来看一下链表的单向与双向。

如上图所示,我们可以看到,单向链表 的节点相对比较简单,在第一个节点里有着指向第二个节点指针,第二个节点也有着指向第三个节点的指针,总的来说,即每个节点都包含数据域和指向下一个节点的指针域,只能单向遍历 。但是在双向链表 中,在每一个节点里包含着数据域与指向下一个节点的指针域以及指向前一个节点的指针域,支持双向遍历。
循环链表与不循环链表

不循环链表指的是链表尾节点的next指针指向的是空指针NULL,而不循环链表的尾节点的next指针则是指向链表的第一个节点,此时的链表形成了一个闭环。在判断是否为循环链表时,我们只需要判断链表尾节点的next指针是否为空指针即可。好的,学到这里,我们可以清楚地判断出上篇文章中讲到的链表为不带头单向不循环链表(简称:单链表) 我们本篇要学习到的链表为**带头双向循环链表(简称:双向链表)**刚好与单链表相反。
双向链表
双向链表的结构
双向链表结构如下图所示:

链表是由节点组成的,当我们要定义一个双向链表的结构时,实际上就是定义节点的结构,那么在双向链表中,它的节点又是由什么构成的呢?看图我们可以很清楚地看到一个节点的组成,首先第一个组成部分就是我们节点所存储的数据,除此之外它还有两个指针,一个后继指针指向当前节点的下一个节点,另一个前驱指针指向当前节点的前一个节点。我们来回忆一下,当我们要把节点的结构以代码形式实现,在单链表中的节点为struct SListNode,如下图。而我们的双向链表的节点就叫struct ListNode。
单链表节点结构 双向链表节点结构


同样的,在这里我们延续了之前的习惯,节点中不一定只存储固定一种类型的数据,所以在这里我们对要存储的数据结构进行一个名称的更改,方便后续根据需要所存储数据的不同来进行相对应的替换,同时也将节点名称进行了简化。
双向链表的初始化
我们来看一下单链表与双向链表在空状态时的区别:
单链表: 没有任何节点,指针直接为空
双向链表:此时链表中只剩下一个头节点
它们初始化的状态是有所不同的,这意味着我们要去创建一个双向链表时,这个链表必须要初始化一个哨兵位,我们才能去往链表里面去进行插入数据和删除数据等操作。接下来我们来实现双向链表的初始化。
我们先来创建一个哨兵位,用mallo函数去给哨兵位申请一块空间,当然申请也不一定都是成功的,所以在这里我们分两种情况。

大家先思考一下,我们现在开辟了一块空间,该怎么去初始化哨兵位的prev和next指针呢?在上篇中我们已经学会了单链表的初始化,是否可以类似地单链表将哨兵位的prev指针与next指针初始化为NULL呢?我们假设它可以初始化为空指针,如下图:

通过这个示意图,我们可以很明显的看到这不是一个双向链表,在文章的开头我们介绍过了什么叫做双向链表,顾名思义,双向链表指的是带头的,双向的,且是循环的链表。在上图中它并没有满足循环的条件,所以我们并不能只是简单的把两个指针置为空。我们要做的是让它循环起来,循环也就意味着我们node的prev指针和next指针,都要指向节点自己,每申请一个节点我们都让它自循环。正确申请节点代码如下:

有了节点申请,我们可以接着来实现双向链表的初始化。
双向链表的数据插入方法
双向链表数据的尾部插入
好的,基于前面的实现,我们再来探讨一下双向链表的数据插入。在进行代码实现操作前,我们再来思考一个问题。如下图,这里函数要传的参数应该是一级指针还是二级指针呢?和大家说明一下**哨兵位节点不能被删除,节点的地址也不能发生改变。**先说答案:传一级指针即可。接下来我来解释一下:
-
节点 = 一个房间,房间里存着数据和前后房间的门牌号(
prev/next指针)。 -
一级指针
LTNode* phead= 一张写着「哨兵位房间地址」的纸条。 -
哨兵位 = 一个永远不会改变位置、不会被拆的固定房间,它的地址永远不变。
-
我们现在要做的就是在链表最后,加一个新房间。
-
我们拿着纸条 phead,找到了哨兵位房间。
-
从哨兵位房间开始,一直找到了当前链表的最后一个房间(尾节点)的门牌号。
-
新建了一个房间(新节点),把它的前门牌号(prev指针)改成尾节点的地址,后门牌号(next指针)改成哨兵位的地址。
-
你再把尾节点的后门牌号改成新房间的地址,把哨兵位的前门牌号也改成新房间的地址。
我们在整个过程中只修改了房间之间的连接,不用改动那张写着「哨兵位房间地址」的纸条,所以只需要一级指针即可。如果非要传二级指针,就需要保证我们的哨兵位不被修改。总的来说:一级指针即可,二级指针大可不必。

理解了传参,我们来看看具体实现。

可以看到我们需要修改的指针有四个,分别为newnode的next,newnode的prev指针,phead的prev指针以及当前尾节点d3的next指针,为了不改变原链表,我们先修改新节点newnode内部的指针的指向,再接着去修改哨兵位和尾节点的指针,理清了基本思路,我们进入代码实现环节。代码如下:
注意红色方框内的代码顺序不能颠倒!! !
双向链表数据的头部插入
趁热打铁,顺着刚才的逻辑,我们再来看一下双向链表数据的头部插入。
依旧画图分析代码思路,注意这里我们讲到的头部数据的插入是指在哨兵位的后面的第一个有效节点之前插入,不是在哨兵位的前面,插入在哨兵位前面的话就变成了我们刚才讲到的尾插了。

和刚才的链表尾插一样,也是需要改变四个指针,与上面思路类似,这里我们就不再过多阐述。直接进入代码的实现环节。代码实现如下:

同样地,函数的后两行代码的顺序不能发生交换。
在pos位置之后插入数据
掌握在双向链表的表头、表尾插入逻辑后,我们进一步拓展功能。讲解如何在指定位置之后插入新节点,以此满足灵活的链表数据新增需求。这里只展示了在指定位置之后插入数据,因为在指定位置之前与之后插入数据思路类似,就不再展示了,感兴趣的大佬们可以自行去实现。
在此之前我们先来实现数据查找的方法,方便后续实现数据插入。

我们可以看到这里有两个参数phead以及我们要查找的数据x,数据查找就是遍历我们的双向链表。我们定义一个变量pcur指向我们的头节点,再使用while循环来遍历链表,只要pcur不等于phead,pcur就接着往下走,直到找到我们pcur->data == x ,返回pcur,也就是我们要查找的数据的地址。
到这里,数据查找的逻辑就实现完毕了。拿到目标节点的地址pcur后,我们就可以基于它,实现双向链表中更灵活的插入操作 ------在指定节点之后插入新数据。
我们先来画图梳理逻辑。

我们可以看到,与前面我们讲到的数据的头插和尾插一样,同样需要改变四个指针,思路也与上面的类似。具体代码实现如下:

双向链表数据的删除方法
双向链表数据的尾删
掌握了双向链表的插入原理,我们接着探讨其数据删除的具体表现。下面结合示意图讲解代码实现思路。

在这里我们避免代码出现phead->prev->prev->next = phead这样代码语句篇幅过长的情况,同时将d3节点的地址存储起来,我们定义一个变量del让它等于哨兵位prev指针指向的节点,也就是我们要删除的那个节点。理清整体实现思路与逻辑原理后,接下来我们正式进行代码编写实现。

双向链表数据的头删
这里也是和上面同样的思路,示意图和代码实现奉上,大佬们可以先通过示意图理解逻辑,再来看下面的代码实现。


删除pos的节点
这部分就不详细解释了,思路和前面讲的实现方法相似,相信聪明的你通过代码也能知晓其中的逻辑。代码如下:

双向链表的销毁
至此双向链表常用操作均已完成,接下来实现节点销毁功能。我们一如既往地画图寻找思路。



双向链表的销毁其实和我们的单链表的销毁是差不多的。我们已知一个哨兵位头节点,让pcur指向我们链表的第一个有效的节点,当我们要删除d1这个节点时,pcur怎么走到d2这个节点呢?目前来说显然是不行的,所以我们在删除前先把下一个节点存储起来,再把pcur所在的节点销毁,销毁完成后,让pcur走到next指针指向的节点,next指针继续往后走,循环下去。这里我们用到while循环条件是pcur != phead,但当我们跳出循环时,我们的哨兵位还没有销毁,是不用嘛?当然不是。哨兵位也是动态申请的,所以出了循环后,我们还要去销毁哨兵位。OK,理清思路后,下面进入代码实现环节。代码如下:

打印链表
好的,当大家看到这里时,本篇博客已进入尾声。最后我们来写一下链表的打印,方便我们更直观地感受链表的整体结构和验证代码的对错。代码如下:

在完成双向带头循环链表的全套功能实现后,我们编写了专用的 ListTest() 测试函数,对初始化、头尾增删、遍历打印、查找销毁等接口逐一进行验证,看看我们辛辛苦苦写的代码,是否能稳稳实现预期效果。

好的,那这篇内容就先整理到这里,这次把双向链表的相关知识整理了一下,为了方便自己日后复习回顾,也希望能给正在学习的小伙伴一些参考。如果文中有理解不到位或是书写有误的地方,欢迎大家在评论区指出,一起交流共同进步。谢谢大家!