前言:
👉 为什么选择双向链表?
在图书馆找书时,单链表只能从头开始逐本翻阅,而双向链表却允许你自由地向前查阅目录或向后浏览内容------这正是双向链表的独特优势!
🌟 双向链表的功能图如下所示:

一、🛠️双向链表的结构
1.1双向链表的图示
🔍 链表长啥样 ?哨兵节点打前阵,双向循环结构
1.2双向链表的定义
cpp
//方便更改数据类型
typedef int LTDataType;
//定义双向链表
typedef struct ListNode
{
//存储数据
LTDataType data;
//指向前驱节点
struct ListNode * next;
//指向后继节点
struct ListNode * prev;
}ListNode;
哨兵位循环结构:
头节点不存数据,next 指向第一个有效节点,prev 指向最后一个节点 ,尾节点的 next 指向头节点,形成闭环。
如图所示:
空链表时,头节点的 next/prev 都指向自己(避免 NULL 判断)
如图所示:
二、🔍双向链表的实现
🛠️ 从 0 到 1 实现:10 个核心函数 + 灵魂注释
💡我们通过三个文件实现双向链表
✅List.h文件 双向链表函数的声明 及 双向链表结构体的实现
✅List.c文件 双向链表函数的实现
✅Test.c文件 测试双向链表函数
2.1双向链表创建节点
代码示例:
cpp
ListNode* SetNode(LTDataType x)
{
ListNode* node = (ListNode *)malloc(sizeof(ListNode));
if (node == NULL)
{
perror("malloc fail");
exit(1);
}
node->data = x;
//为了达到循环的目的,将前驱指针和后驱指针都指向自己
node->next = node->prev = node;
return node;
}
代码解析:
👉通过使用malloc动态开辟空间,创建一个节点。
👉核心:为了达到循环的目的,将前驱指针和后驱指针都指向直接,达到双向的目的。
如下图所示:
2.2双向链表的初始化
代码示例:
cpp
void LTInit(ListNode** pphead)
{
assert(pphead);
//给链表创建一个哨兵位
//不关心哨兵位的值,这里赋值-1
*pphead = SetNode(-1);
}
👉为什么这里函数参数为:ListNode** pphead ??
💡要理解 LTInit 用二级指针的原因,核心是搞懂 C 语言值传递规则 以及函数的实际目的------ 我们需要通过函数修改外部指针变量本身的指向,而非仅访问指针指向的内容。
💡例如:如果传普通变量(如 int a),函数改的是 a 的副本,不影响外部原变量。
💡例如:如果传一级指针(如 ListNode* phead),函数改的是指针副本的指向,也不影响外部原指针的指向,只有传指针的地址(即二级指针 ListNode** pphead),才能通过地址间接操作外部原指针,修改它的指向。
💡LTInit
的核心目的:给外部头指针 "赋值",外部使用双向链表时,往往会先定义一个头指针,例如:ListNode * plist=NULL;所以这里函数参数要为二级指针,而不应该为一级指针。
2.3双向链表的打印
代码示例:
cpp
void LTPrint(ListNode* phead)
{
//保证双向链表有效,即哨兵节点不为空。
assert(phead);
//定义一个pcur指针进行遍历每个节点
ListNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
代码解析:
👉定义一个pcur指针变量保存的是哨兵位节点的下一个节点,通过while循环进行遍历整个单链表。
👉值得注意的是while循环的判定条件,因为是循环链表,所以尾节点的后继指针存储的是哨兵位节点,故而遍历完双向链表节点后,会重新回到哨兵节点,所以这里的判定条件为pcur!=phead
2.4双向链表的尾插
代码示例:
cpp
void LTPushBack(ListNode* phead, LTDataType x)
{
//保证双向链表有效,即哨兵节点不为空。
assert(phead);
//创建一个新节点
ListNode* newnode = SetNode(x);
//尾插一个节点会影响到
//phead(哨兵节点) phead->prev(尾节点) newnode(新节点)
//为了不影响原链表节点,优先修改新链表的前驱和后继
newnode->prev = phead->prev;
newnode->next = phead;
//再修改尾节点和哨兵节点的指向
phead->prev->next = newnode;
phead->prev = newnode;
}
代码解析:
以如下示意图为例
🔥首先要理解,尾插一个节点会影响到哪些节点:phead(哨兵节点) 尾节点(phead->prev) 新节点(newnode)。
🔥为了先不影响原链表的逻辑,我们优先修改新节点(newnode)的前驱指针和后继指针。
如③所示:newnode->prev=phead->prev;
如④所示:newnode->next=phead;
🔥再修改原链表的逻辑,修改哨兵节点的前驱指针和尾节点的后继指针。
如①所示:phead->prev->next=newnode;
如②所示:phead->prev=newnode;
🚦温馨提示:不要将①代码和②代码进行调换顺序,否则将导致因为哨兵节点的前驱指针优先改变,而导致找不到尾节点。
🔍如果是双向链表中只有一个哨兵节点是否满足上述代码呢?
我们要知道,如果只有一个哨兵节点时,哨兵节点的前驱phead->prev=phead; 哨兵节点的后驱phead->next=phead;
哨兵节点的前驱和后继指针都指向的是哨兵节点本身。
如下图所示:
🔥为了先不影响原链表的逻辑,我们优先修改新节点(newnode)的前驱指针和后继指针。
如①所示:newnode->prev=phead->prev; (这里的phead->prev存放的就是phead节点)
如②所示:newnode->next=phead;
🔥再修改原链表的逻辑,修改哨兵节点的前驱指针和尾节点的后继指针。
如④所示:phead->prev->next=newnode; (phead->prev存放的就是phead节点,所以这里就相当于phead->next)
如③所示:phead->prev=newnode; (这里将原来存储的phead节点改为了newnode节点)
🔍通过检验,我们发现只有一个哨兵节点时,也满足上述代码。
2.5双向链表的头插
代码示例:
cpp
void LTPushFront(ListNode* phead, LTDataType x)
{
//保证双向链表有效,即哨兵节点不为空。
assert(phead);
//创建新节点
ListNode* newnode = SetNode(x);
//头插一个节点会影响到
//phead(哨兵节点) phead->next(哨兵节点的下一个节点) newnode(新节点)
//为了不影响到原链表逻辑,先更改新节点的前驱和后继
newnode->prev = phead;
newnode->next = phead->next;
//再更改哨兵节点 和 哨兵节点的下一个节点
phead->next->prev = newnode;
phead->next = newnode;
}
代码解析:
如下图所示:
🔥首先要理解,尾插一个节点会影响到哪些节点:phead(哨兵节点) 节点1(phead->next) 新节点(newnode)。
🔥为了先不影响原链表的逻辑,我们优先修改新节点(newnode)的前驱指针和后继指针。
如①所示:newnode->prev=phead;
如②所示:newnode->next=phead;
🔥再修改原链表的逻辑,修改节点1的前驱指针和哨兵节点的后继指针。
如③所示:phead->next->prev=newnode;
如④所示: phead->next=newnode;
温馨提示:代码③和代码④顺序不能进行调换,如果先进行代码④,则将找不到节点1,后续无法对节点1的前驱进行修改。
🔍如果是双向链表中只有一个哨兵节点是否满足上述代码呢?
我们要知道,如果只有一个哨兵节点时,哨兵节点的前驱phead->prev=phead; 哨兵节点的后驱phead->next=phead;
哨兵节点的前驱和后继指针都指向的是哨兵节点本身。
如下图所示:
🔥为了先不影响原链表的逻辑,我们优先修改新节点(newnode)的前驱指针和后继指针。
如①所示:newnode->prev=phead->prev; (这里的phead->prev存放的就是phead节点)
如②所示:newnode->next=phead;
🔥再修改原链表的逻辑,修改节点1的前驱指针和哨兵节点的后继指针。
如③所示:phead->next->prev=newnode; (这里phead->next存放的就是phead节点,对phead节点的前驱进行改变)
如④所示:phead->next=newnode;(这里对哨兵节点的后继进行改变)
🔍通过检验,我们发现只有一个哨兵节点时,也满足上述代码。
2.6双向链表的尾删
代码示例:
cpp
void LTPopBack(ListNode* phead)
{
//保证双向链表有效,即哨兵节点不为空。
//保证双向链表有其他节点
assert(phead && phead->next != phead);
//删除尾节点 影响到的节点有:
//phead(哨兵节点) phead->prev(尾节点) phead->prev->prev(尾节点的前一个节点)
//对哨兵节点前驱进行修改后,会找不到尾节点,所以要临时保存尾节点
//此时尾节点为 tmp 尾节点的前一个节点为 tmp->prev
ListNode* tmp = phead->prev;
//修改哨兵节点的前驱
phead->prev = tmp->prev;
//修改尾节点的前一个节点
tmp->prev->next = phead;
free(tmp);
tmp = NULL;
}
代码解析:
如下图所示:
🔥首先我们需要明白,什么时候对双向链表进行删除,当双向链表除哨兵节点外,还要有其他节点,所以这里要进行断言判定,assert(phead && phead->next != phead);
🔥要理解删除尾节点时,会影响到哪些节点,phead(哨兵节点) phead->prev(尾节点) phead->prev->prev(尾节点前一个节点)
🔥在修改哨兵节点的前驱后,将无法找到尾节点,所以我们需要定义临时变量进行保存尾节点,ListNode* tmp = phead->prev。
此时的尾节点为:tmp 尾节点的前一个节点为:tmp->prev
🔥最后修改哨兵节点的前驱和尾节点的前一个节点的后继,并释放尾节点。
如①所示:phead->prev=tmp->prev;
如②所示:tmp->prev->next=phead;
free(tmp); tmp=NULL;
2.7双向链表的头删
代码示例:
cpp
void LTPopFront(ListNode* phead)
{
//保证双向链表有效,即哨兵节点不为空。
//保证双向链表有其他节点
assert(phead && phead->next != phead);
//删除头节点的下一个节点 影响到的节点有:
//phead(哨兵节点) phead->next(哨兵节点后的第一个节点) phead->next->next(哨兵节点后的第二个节点)
//对哨兵节点的后驱进行修改,会找不到哨兵节点后的第一个节点,所以要先进行保存
//此时哨兵节点后的第一个节点为:tmp 哨兵节点后的第二个节点为:tmp->next
ListNode* tmp = phead->next;
//修改哨兵节点后的第一个节点的后继
phead->next = tmp->next;
//修改哨兵节点后的第二个节点的前驱
tmp->next->prev = phead;
free(tmp);
tmp = NULL;
}
代码解析:
如下图所示:
🔥首先我们需要明白,什么时候对双向链表进行删除,当双向链表除哨兵节点外,还要有其他节点,所以这里要进行断言判定,assert(phead && phead->next != phead);
🔥要理解删除头节点时,会影响到哪些节点,phead(哨兵节点) phead->next(节点1) phead->next->next(节点2)
🔥在修改哨兵节点的后继后,将无法找到节点1,所以我们需要临时保存节点1,ListNode* tmp = phead->next;
此时节点1为:tmp 节点2为:tmp->next;
🔥最后修改哨兵节点的后继指针和节点2的前驱指针,最后释放节点1,并将其置空
如①所示:phead->next=tmp->next;
如②所示:tmp->next->prev=phead;
free(tmp); tmp=NULL;
2.8双向链表的查找
代码示例:
cpp
ListNode* LTFind(ListNode* phead, LTDataType x)
{
//保证双向链表有效,即哨兵节点不为空。
assert(phead);
//定义一个pcur遍历双向链表
ListNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//没有找到该元素
return NULL;
}
代码解析:
👉双向链表的查找与双向链表的打印逻辑类似,需要定义一个pcur变量遍历整个双向链表,这里就不在过多赘述。
2.9双向链表指定插入
代码示例:
cpp
//在指定位置之后插入一个新节点
void LTInsert(ListNode* pos, LTDataType x)
{
//保证pos位置有效
assert(pos);
//申请一个新节点
ListNode* newnode = SetNode(x);
//在指定位置插入一个节点,要影响到的节点有
//pos(指定位置的节点) newnode(新申请的节点) pos->next(pos位置之后的节点)
//为了不影响原链表的逻辑
//优先改变新链表的指向
newnode->prev = pos;
newnode->next = pos->next;
//修改pos位置之后的节点前驱指向
pos->next->prev = newnode;
//再修改pos位置的后驱指向
pos->next = newnode;
}
代码解析:
如下图所示:
🔥首先我们要了解插入一个新节点,会影响到哪些节点:pos(指定位置节点) newnode(新节点) pos->next(指定位置后的一个节点)
🔥为了不影响原链表节点的逻辑,我们优先修改新节点的前驱指针和后继指针。
如①所示:newnode->prev=pos;
如②所示:newnode->next=pos;
🔥后续再修改原链表的逻辑,修改pos后一个节点的前驱指针 和 pos节点的后继指针
如③所示:pos->next->prev=newnode;
如④所示:pos->next=newnode;
2.10双向链表指定删除
代码示例:
cpp
void LTErase(ListNode* phead,ListNode* pos)
{
//确保pos不为空
assert(pos&&phead&&pos!=phead);
//在指定位置删除一个节点要影响到的节点有
//pos->prev(指定位置的前一个节点) pos(指定位置的节点) pos->next(指定位置的下一个节点)
//修改指定位置的前一个节点的后继
pos->prev->next = pos->next;
//修改指定位置的下一个节点的前驱
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
代码解析:
如下图所示:
🔥首先我们要了解删除指定位置的节点,会影响到哪些节点:pos(指定位置节点) pos->prev(指定位置前的节点) pos->next(指定位置后的一个节点)
🔥****修改指定位置前的节点的后继指针 和 指定位置后的一个节点的前驱指针
如图①所示:pos->prev->next=pos->next;
如图②所示:pos->next->prev=pos->prev;
🔥****最后将pos位置节点的指针进行释放,并将其置为空。
2.11销毁双向链表
代码示例:
cpp
void LTDestroy(ListNode* phead)
{
//确保头指针地址有效
assert(phead);
//定义pcur变量进行变量双向链表
ListNode* pcur = phead->next;
while (pcur != phead)
{
//用临时变量tmp来保存pcur的下一个节点位置
ListNode* tmp = pcur->next;
free(pcur);
pcur = tmp;
}
//释放哨兵节点,这里传入的是一级指针,改变形参不影响实参
//所以销毁双向链表后要手动置空,否则头节点指针变成了野指针
free(phead);
phead = NULL;
}
代码解析:
👉这里通过定义pcur指针对双向链表进行遍历,通过临时变量tmp保存pcur下一个节点,如果不定义临时遍历,释放当前节点的空间之后,将找不到下一个节点。
👉free(phead);:释放哨兵节点的内存(此时有效数据节点已全部释放,最后释放哨兵节点)。
🚦****特别注意:phead = NULL;:将函数内部的 phead 指针置空。但由于是值传递,这个操作不会影响调用者传入的外部指针(比如 main 中定义的 ListNode* plist)。因此调用 LTDestroy 后,外部指针会变成野指针,需要手动在外部置空(如 plist = NULL;)。
三、💡完整源码
3.1List.h文件
cpp
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
//方便更改数据类型
typedef int LTDataType;
//定义双向链表
typedef struct ListNode
{
//存储数据
LTDataType data;
//指向下一个节点的指针
struct ListNode * next;
struct ListNode * prev;
}ListNode;
//实现双向链表的功能
//创建一个节点
ListNode* SetNode(LTDataType x);
//初始化头节点,要改变头节点需要二级指针
void LTInit(ListNode** phead);
//打印各个节点
void LTPrint(ListNode* phead);
//尾插节点
//不改变哨兵位节点,一级指针即可
void LTPushBack(ListNode* phead,LTDataType x);
//头插节点
void LTPushFront(ListNode* phead,LTDataType x);
//尾删节点
void LTPopBack(ListNode* phead);
//头删节点
void LTPopFront(ListNode* phead);
//查找节点
ListNode* LTFind(ListNode* phead, LTDataType x);
//在指定位置之后插入一个节点
void LTInsert(ListNode* pos, LTDataType x);
//删除pos位置的节点
void LTErase(ListNode* phead,ListNode* pos);
//销毁双向链表
void LTDestroy(ListNode* phead);
3.2List.c文件
cpp
#include"List.h"
ListNode* SetNode(LTDataType x)
{
ListNode* node = (ListNode *)malloc(sizeof(ListNode));
if (node == NULL)
{
perror("malloc fail");
exit(1);
}
node->data = x;
//为了达到循环的目的,将前驱指针和后驱指针都指向自己
node->next = node->prev = node;
return node;
}
void LTInit(ListNode** pphead)
{
//给链表创建一个哨兵位
assert(pphead);
//不关心哨兵位的值,这里赋值-1
*pphead = SetNode(-1);
}
void LTPrint(ListNode* phead)
{
//保证双向链表有效,即哨兵节点不为空。
assert(phead);
//定义一个pcur指针进行遍历每个节点
ListNode* pcur = phead->next;
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
void LTPushBack(ListNode* phead, LTDataType x)
{
//保证双向链表有效,即哨兵节点不为空。
assert(phead);
//创建一个新节点
ListNode* newnode = SetNode(x);
//尾插一个节点会影响到
//phead(哨兵节点) phead->prev(尾节点) newnode(新节点)
//为了不影响原链表节点,优先修改新链表的前驱和后继
newnode->prev = phead->prev;
newnode->next = phead;
//再修改尾节点和哨兵节点的指向
phead->prev->next = newnode;
phead->prev = newnode;
}
void LTPushFront(ListNode* phead, LTDataType x)
{
//保证双向链表有效,即哨兵节点不为空。
assert(phead);
//创建新节点
ListNode* newnode = SetNode(x);
//头插一个节点会影响到
//phead(哨兵节点) phead->next(哨兵节点的下一个节点) newnode(新节点)
//为了不影响到原链表逻辑,先更改新节点的前驱和后继
newnode->prev = phead;
newnode->next = phead->next;
//再更改哨兵节点 和 哨兵节点的下一个节点
phead->next->prev = newnode;
phead->next = newnode;
}
void LTPopBack(ListNode* phead)
{
//保证双向链表有效,即哨兵节点不为空。
//保证双向链表有其他节点
assert(phead && phead->next != phead);
//删除尾节点 影响到的节点有:
//phead(哨兵节点) phead->prev(尾节点) phead->prev->prev(尾节点的前一个节点)
//对哨兵节点前驱进行修改后,会找不到尾节点,所以要临时保存尾节点
//此时尾节点为 tmp 尾节点的前一个节点为 tmp->prev
ListNode* tmp = phead->prev;
//修改哨兵节点的前驱
phead->prev = tmp->prev;
//修改尾节点的前一个节点
tmp->prev->next = phead;
free(tmp);
tmp = NULL;
}
void LTPopFront(ListNode* phead)
{
//保证双向链表有效,即哨兵节点不为空。
//保证双向链表有其他节点
assert(phead && phead->next != phead);
//删除头节点的下一个节点 影响到的节点有:
//phead(哨兵节点) phead->next(哨兵节点后的第一个节点) phead->next->next(哨兵节点后的第二个节点)
//对哨兵节点的后驱进行修改,会找不到哨兵节点后的第一个节点,所以要先进行保存
//此时哨兵节点后的第一个节点为:tmp 哨兵节点后的第二个节点为:tmp->next
ListNode* tmp = phead->next;
//修改哨兵节点后的第一个节点的后继
phead->next = tmp->next;
//修改哨兵节点后的第二个节点的前驱
tmp->next->prev = phead;
free(tmp);
tmp = NULL;
}
ListNode* LTFind(ListNode* phead, LTDataType x)
{
//保证双向链表有效,即哨兵节点不为空。
assert(phead);
//定义一个pcur遍历双向链表
ListNode* pcur = phead->next;
while (pcur != phead)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;
}
//没有找到该元素
return NULL;
}
//在指定位置之后插入一个新节点
void LTInsert(ListNode* pos, LTDataType x)
{
//保证pos位置有效
assert(pos);
//申请一个新节点
ListNode* newnode = SetNode(x);
//在指定位置插入一个节点,要影响到的节点有
//pos(指定位置的节点) newnode(新申请的节点) pos->next(pos位置之后的节点)
//为了不影响原链表的逻辑
//优先改变新链表的指向
newnode->prev = pos;
newnode->next = pos->next;
//修改pos位置之后的节点前驱指向
pos->next->prev = newnode;
//再修改pos位置的后驱指向
pos->next = newnode;
}
void LTErase(ListNode* phead,ListNode* pos)
{
//确保pos不为空
assert(pos&&phead&&pos!=phead);
//在指定位置删除一个节点要影响到的节点有
//pos->prev(指定位置的前一个节点) pos(指定位置的节点) pos->next(指定位置的下一个节点)
//修改指定位置的前一个节点的后继
pos->prev->next = pos->next;
//修改指定位置的下一个节点的前驱
pos->next->prev = pos->prev;
free(pos);
pos = NULL;
}
void LTDestroy(ListNode* phead)
{
//确保头指针地址有效
assert(phead);
//定义pcur变量进行变量双向链表
ListNode* pcur = phead->next;
while (pcur != phead)
{
//用临时变量tmp来保存pcur的下一个节点位置
ListNode* tmp = pcur->next;
free(pcur);
pcur = tmp;
}
//释放哨兵节点,这里传入的是一级指针,改变形参不影响实参
//所以销毁双向链表后要手动置空,否则头节点指针变成了野指针
free(phead);
phead = NULL;
}
四、 🌟 双向链表代码演示
4.1尾插节点展示
cpp
void test01()
{
ListNode* plist = NULL;
//创建哨兵位plist
LTInit(&plist);
//尾插三个节点
LTPushBack(plist, 1);
LTPrint(plist);
LTPushBack(plist, 2);
LTPrint(plist);
LTPushBack(plist, 3);
LTPrint(plist);
}

4.2头插节点展示
cpp
void test02()
{
ListNode* plist = NULL;
//创建哨兵位plist
LTInit(&plist);
//头插三个节点
LTPushFront(plist, 4);
LTPrint(plist);
LTPushFront(plist, 5);
LTPrint(plist);
LTPushFront(plist, 6);
LTPrint(plist);
}

4.3尾删节点
cpp
void test01()
{
ListNode* plist = NULL;
//创建哨兵位plist
LTInit(&plist);
printf("尾插节点:\n");
//尾插三个节点
LTPushBack(plist, 1);
LTPrint(plist);
LTPushBack(plist, 2);
LTPrint(plist);
LTPushBack(plist, 3);
LTPrint(plist);
printf("尾删节点\n");
//尾删节点
LTPopBack(plist);
LTPrint(plist);
LTPopBack(plist);
LTPrint(plist);
LTPopBack(plist);
LTPrint(plist);
}

4.4头删节点
cpp
void test02()
{
ListNode* plist = NULL;
//创建哨兵位plist
LTInit(&plist);
printf("头插节点\n");
//头插三个节点
LTPushFront(plist, 4);
LTPrint(plist);
LTPushFront(plist, 5);
LTPrint(plist);
LTPushFront(plist, 6);
LTPrint(plist);
printf("头删节点\n");
LTPopFront(plist);
LTPrint(plist);
LTPopFront(plist);
LTPrint(plist);
LTPopFront(plist);
LTPrint(plist);
}

4.5查找节点
情况一:成功查找到节点
cpp
void test02()
{
ListNode* plist = NULL;
//创建哨兵位plist
LTInit(&plist);
printf("头插节点\n");
//头插三个节点
LTPushFront(plist, 4);
LTPrint(plist);
LTPushFront(plist, 5);
LTPrint(plist);
LTPushFront(plist, 6);
LTPrint(plist);
printf("开始查找节点\n");
ListNode* find = LTFind(plist, 6);
if (find != NULL)
{
printf("找到了,查找结果为:%d\n", find->data);
}
else
{
printf("未查找到\n");
}
}

情况二:未查找到节点
cpp
void test02()
{
ListNode* plist = NULL;
//创建哨兵位plist
LTInit(&plist);
printf("头插节点\n");
//头插三个节点
LTPushFront(plist, 4);
LTPrint(plist);
LTPushFront(plist, 5);
LTPrint(plist);
LTPushFront(plist, 6);
LTPrint(plist);
printf("开始查找节点\n");
ListNode* find = LTFind(plist, 0);
if (find != NULL)
{
printf("找到了,查找结果为:%d\n", find->data);
}
else
{
printf("未查找到\n");
}
}

4.6指定位置后插入节点
cpp
void test02()
{
ListNode* plist = NULL;
//创建哨兵位plist
LTInit(&plist);
printf("头插节点\n");
//头插三个节点
LTPushFront(plist, 4);
LTPrint(plist);
LTPushFront(plist, 5);
LTPrint(plist);
LTPushFront(plist, 6);
LTPrint(plist);
ListNode* find = LTFind(plist, 6);
printf("在节点值为6的后面,插入一个节点值为99\n");
LTInsert(find, 99);
LTPrint(plist);
}

4.7删除指定位置节点
cpp
void test02()
{
ListNode* plist = NULL;
//创建哨兵位plist
LTInit(&plist);
printf("头插节点\n");
//头插三个节点
LTPushFront(plist, 4);
LTPrint(plist);
LTPushFront(plist, 5);
LTPrint(plist);
LTPushFront(plist, 6);
LTPrint(plist);
ListNode* find = LTFind(plist, 6);
printf("在节点值为6的后面,插入一个节点值为99\n");
LTInsert(find, 99);
LTPrint(plist);
find = LTFind(plist, 99);
printf("删除节点值为99的节点\n");
LTErase(plist,find);
LTPrint(plist);
}

4.8销毁链表
cpp
void test02()
{
ListNode* plist = NULL;
//创建哨兵位plist
LTInit(&plist);
printf("头插节点\n");
//头插三个节点
LTPushFront(plist, 4);
LTPrint(plist);
LTPushFront(plist, 5);
LTPrint(plist);
LTPushFront(plist, 6);
LTPrint(plist);
ListNode* find = LTFind(plist, 6);
printf("在节点值为6的后面,插入一个节点值为99\n");
LTInsert(find, 99);
LTPrint(plist);
find = LTFind(plist, 99);
printf("删除节点值为99的节点\n");
LTErase(plist,find);
LTPrint(plist);
//销毁整个双链表
LTDestroy(plist);
//置为空
plist = NULL;
}

五、✅总结与反思
🌟双链表核心总结
💡双链表是一种每个节点包含两个指针(prev 指向前驱、next 指向后继)的线性数据结构,支持双向遍历,常结合哨兵位(头节点)和循环结构设计,以简化边界操作。
1. 结构优势
①双向遍历:可从任意节点向前 / 向后访问,适合需要 "回退" 的场景(如浏览器历史、文本编辑器撤销)。
②O (1) 时间删除已知节点:已知节点位置时,无需像单链表那样遍历找前驱(单链表删节点需 O (n))。
③哨兵位简化边界:带头节点(哨兵位)的双链表,空链表、头尾操作时无需额外判断 NULL,代码更简洁。
2. 核心操作逻辑(以 "带头循环双链表" 为例)
👉初始化:
①创建哨兵节点,让其 prev 和 next 指向自身,形成循环。
👉插入(头插 / 尾插 / 指定位置插入):
①先修改新节点的 prev 和 next(连接到目标前驱 / 后继);
②再修改前驱节点的 next 和 后继节点的 prev(确保原链表指针同步更新)。(顺序不能反,否则会丢失节点引用)
👉删除(头删 / 尾删 / 指定位置删除):
①先保存目标节点的前驱和后继;
②修改前驱的 next 和后继的 prev(跳过目标节点);
③free 目标节点,防止内存泄漏。
👉销毁:
①遍历释放所有有效节点后,释放哨兵节点,并手动置空外部指针(避免野指针)
既然看到这里了,不妨点赞+收藏,感谢大家,若有问题请指正。
