前言在图书馆找书时单链表只能从头开始逐本翻阅而双向链表却允许你自由地向前查阅目录或向后浏览内容——这正是双向链表的独特优势双向链表的功能图如下所示一、双向链表的结构1.1双向链表的图示 链表长啥样 哨兵节点打前阵双向循环结构1.2双向链表的定义//方便更改数据类型 typedef int LTDataType; //定义双向链表 typedef struct ListNode { //存储数据 LTDataType data; //指向前驱节点 struct ListNode * next; //指向后继节点 struct ListNode * prev; }ListNode;①哨兵位循环结构头节点不存数据next 指向第一个有效节点prev 指向最后一个节点尾节点的 next 指向头节点形成闭环。如图所示②空链表时头节点的 next/prev 都指向自己避免 NULL 判断如图所示二、双向链表的实现我们通过三个文件实现双向链表①List.h文件 双向链表函数的声明 及 双向链表结构体的实现②List.c文件 双向链表函数的实现③Test.c文件 测试双向链表函数2.1双向链表创建节点代码示例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双向链表的初始化代码示例void LTInit(ListNode** pphead) { assert(pphead); //给链表创建一个哨兵位 //不关心哨兵位的值这里赋值-1 *pphead SetNode(-1); }为什么这里函数参数为ListNode** pphead 要理解 LTInit 函数用二级指针的原因核心是搞懂 C 语言值传递规则 以及函数的实际目的—— 我们需要通过函数修改外部指针变量本身的指向而非仅访问指针指向的内容。①如果传普通变量如 int a函数改的是 a 的副本不影响外部原变量。②如果传一级指针如 ListNode* phead函数改的是指针副本的指向也不影响外部原指针的指向,只有传指针的地址即二级指针 ListNode** pphead才能通过地址间接操作外部原指针修改它的指向。③LTInit的核心目的给外部头指针 “赋值”外部使用双向链表时往往会先定义一个头指针。例如ListNode * plistNULL 所以这里函数参数要为二级指针而不应该为一级指针。2.3双向链表的打印代码示例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循环的判定条件因为是循环链表所以尾节点的后继指针存储的是哨兵位节点故而遍历完双向链表节点后会重新回到哨兵节点所以这里的判定条件为pcurphead2.4双向链表的尾插代码示例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)。A.为了先不影响原链表的逻辑我们优先修改新节点(newnode)的前驱指针和后继指针。如③所示newnode-prevphead-prev;如④所示newnode-nextphead;B.再修改原链表的逻辑修改哨兵节点的前驱指针和尾节点的后继指针。如①所示phead-prev-nextnewnode;如②所示phead-prevnewnode;温馨提示不要将①代码和②代码进行调换顺序否则将导致因为哨兵节点的前驱指针优先改变而导致找不到尾节点。如果是双向链表中只有一个哨兵节点是否满足上述代码呢我们要知道如果只有一个哨兵节点时哨兵节点的前驱phead-prevphead; 哨兵节点的后驱phead-nextphead;哨兵节点的前驱和后继指针都指向的是哨兵节点本身。如下图所示A.为了先不影响原链表的逻辑我们优先修改新节点(newnode)的前驱指针和后继指针。如①所示newnode-prevphead-prev; (这里的phead-prev存放的就是phead节点)如②所示newnode-nextphead;B.再修改原链表的逻辑修改哨兵节点的前驱指针和尾节点的后继指针。如④所示phead-prev-nextnewnode; (phead-prev存放的就是phead节点所以这里就相当于phead-next)如③所示phead-prevnewnode; 这里将原来存储的phead节点改为了newnode节点通过检验我们发现只有一个哨兵节点时也满足上述代码。2.5双向链表的头插代码示例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-prevphead;如②所示newnode-nextphead;再修改原链表的逻辑修改节点1的前驱指针和哨兵节点的后继指针。如③所示phead-next-prevnewnode;如④所示: phead-nextnewnode;温馨提示代码③和代码④顺序不能进行调换如果先进行代码④则将找不到节点1后续无法对节点1的前驱进行修改。如果是双向链表中只有一个哨兵节点是否满足上述代码呢我们要知道如果只有一个哨兵节点时哨兵节点的前驱phead-prevphead; 哨兵节点的后驱phead-nextphead;此时哨兵节点的前驱和后继指针都指向的是哨兵节点本身。如下图所示A.为了先不影响原链表的逻辑我们优先修改新节点(newnode)的前驱指针和后继指针。如①所示newnode-prevphead-prev; (这里的phead-prev存放的就是phead节点)如②所示newnode-nextphead;B.再修改原链表的逻辑修改节点1的前驱指针和哨兵节点的后继指针。如③所示phead-next-prevnewnode; (这里phead-next存放的就是phead节点对phead节点的前驱进行改变)如④所示phead-nextnewnode;(这里对哨兵节点的后继进行改变)通过检验我们发现只有一个哨兵节点时也满足上述代码。2.6双向链表的尾删代码示例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-prevtmp-prev;如②所示tmp-prev-nextphead;free(tmp); tmpNULL;2.7双向链表的头删代码示例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所以我们需要临时保存节点1ListNode* tmp phead-next;此时节点1为tmp 节点2为tmp-next;④最后修改哨兵节点的后继指针和节点2的前驱指针最后释放节点1并将其置空如①所示phead-nexttmp-next;如②所示tmp-next-prevphead;free(tmp); tmpNULL;2.8双向链表的查找代码示例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双向链表指定插入代码示例//在指定位置之后插入一个新节点 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-prevpos;如②所示newnode-nextpos;③后续再修改原链表的逻辑修改pos后一个节点的前驱指针 和 pos节点的后继指针如③所示pos-next-prevnewnode;如④所示pos-nextnewnode;2.10双向链表指定删除代码示例void LTErase(ListNode* phead,ListNode* pos) { //确保pos不为空 assert(pospheadpos!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-nextpos-next;如图②所示pos-next-prevpos-prev;③最后将pos位置节点的指针进行释放并将其置为空。2.11销毁双向链表代码示例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文件#pragma once #includestdio.h #includestdlib.h #includeassert.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文件#includeList.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(pospheadpos!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尾插节点展示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头插节点展示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尾删节点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头删节点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查找节点情况一成功查找到节点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); } }情况二未查找到节点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指定位置后插入节点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删除指定位置节点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销毁链表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. 核心操作逻辑以 “带头循环双链表” 为例A. 初始化①创建哨兵节点让其 prev 和 next 指向自身形成循环。B. 插入头插 / 尾插 / 指定位置插入①先修改新节点的 prev 和 next连接到目标前驱 / 后继②再修改前驱节点的 next 和 后继节点的 prev确保原链表指针同步更新。顺序不能反否则会丢失节点引用C. 删除头删 / 尾删 / 指定位置删除①先保存目标节点的前驱和后继②修改前驱的 next 和后继的 prev跳过目标节点③free 目标节点防止内存泄漏。D. 销毁①遍历释放所有有效节点后释放哨兵节点并手动置空外部指针避免野指针既然看到这里了不妨点赞收藏感谢大家若有问题请指正。