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

从 0 到 1 保姆级实现C语言双向链表

前言:

👉 为什么选择双向链表?

在图书馆找书时,单链表只能从头开始逐本翻阅,而双向链表却允许你自由地向前查阅目录或向后浏览内容——这正是双向链表的独特优势!
     

🌟双向链表的功能图如下所示:

一、🛠️双向链表的结构

        

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 判断)

        

如图所示:

        

        

二、🔍双向链表的实现

       

🛠️ 从 0 到 1 实现:10 个核心函数 + 灵魂注释 

💡我们通过三个文件实现双向链表

        

✅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 * plist=NULL;所以这里函数参数要为二级指针,而不应该为一级指针。

        

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循环的判定条件,因为是循环链表,所以尾节点的后继指针存储的是哨兵位节点,故而遍历完双向链表节点后,会重新回到哨兵节点,所以这里的判定条件为pcur!=phead

        

2.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)。

        

🔥为了先不影响原链表的逻辑,我们优先修改新节点(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双向链表的头插

代码示例:

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双向链表的尾删

代码示例:

void LTPopBack(ListNode* phead)
{//保证双向链表有效,即哨兵节点不为空。 //保证双向链表有其他节点assert(phead && phead->next != phead);//删除尾节点 影响到的节点有://phead(哨兵节点)  phead->prev(尾节点)  phead->prev->prev(尾节点的前一个节点)//对哨兵节点前驱进行修改后,会找不到尾节点,所以要临时保存尾节点//此时尾节点为 tmp  尾节点的前一个节点为 tmp->prevListNode* 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双向链表的头删

代码示例:

void LTPopFront(ListNode* phead)
{//保证双向链表有效,即哨兵节点不为空。 //保证双向链表有其他节点assert(phead && phead->next != phead);//删除头节点的下一个节点  影响到的节点有://phead(哨兵节点) phead->next(哨兵节点后的第一个节点)  phead->next->next(哨兵节点后的第二个节点)//对哨兵节点的后驱进行修改,会找不到哨兵节点后的第一个节点,所以要先进行保存//此时哨兵节点后的第一个节点为:tmp   哨兵节点后的第二个节点为:tmp->nextListNode* 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双向链表的查找

代码示例:

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->prev=pos;

        

        如②所示:newnode->next=pos;

        

🔥后续再修改原链表的逻辑,修改pos后一个节点的前驱指针 和 pos节点的后继指针

        

        如③所示:pos->next->prev=newnode;

        

        如④所示:pos->next=newnode;

        

2.10双向链表指定删除

代码示例:

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销毁双向链表

代码示例:

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
#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文件

#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->prevListNode* 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->nextListNode* 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尾插节点展示

void test01()
{ListNode* plist = NULL;//创建哨兵位plistLTInit(&plist);//尾插三个节点LTPushBack(plist, 1);LTPrint(plist);LTPushBack(plist, 2);LTPrint(plist);LTPushBack(plist, 3);LTPrint(plist);
}

        

4.2头插节点展示

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

        

4.3尾删节点

        

void test01()
{ListNode* plist = NULL;//创建哨兵位plistLTInit(&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;//创建哨兵位plistLTInit(&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;//创建哨兵位plistLTInit(&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;//创建哨兵位plistLTInit(&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;//创建哨兵位plistLTInit(&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;//创建哨兵位plistLTInit(&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;//创建哨兵位plistLTInit(&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 目标节点,防止内存泄漏。


👉销毁:

        

    ①遍历释放所有有效节点后,释放哨兵节点,并手动置空外部指针(避免野指针)

            

     既然看到这里了,不妨点赞+收藏,感谢大家,若有问题请指正。

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

    相关文章:

  • 2 IP地址规划与设计案例分析
  • Vue 中 8 种组件通信方式
  • 十三、vue3后台项目系列——sidebar的实现,递归组件
  • LeetCode 383 - 赎金信
  • compose multiplatform reader3
  • Redis 入门与实践
  • 【OpenGL】texture 纹理
  • agentscope以STUDIO方式调用MCP服务
  • 无公网 IP 访问群晖 NAS:神卓 N600 的安全解决方案(附其他方法风险对比)
  • Redis最佳实践——性能优化技巧之Pipeline 批量操作
  • Java-130 深入浅出 MySQL MyCat 深入解析 核心配置文件 server.xml 使用与优化
  • 业主信息查询功能测试指南
  • WinDivert学习文档之四-————卸载
  • 分布式链路追踪关键指标实战:精准定位服务调用 “慢节点” 全指南(二)
  • DuckDB客户端API之ADBC官方文档翻译
  • 区块链技术应用开发:智能合约进阶与多链生态落地实践
  • 分布式专题——13 RabbitMQ之高级功能
  • 神经风格迁移(Neural Style Transfer)
  • Chromium 138 编译指南 Ubuntu 篇:源码获取与版本管理(四)
  • R 语言入门实战|第九章 循环与模拟:用自动化任务解锁数据科学概率思维
  • [论文阅读] 人工智能 + 软件工程 | 4907个用户故事验证!SEEAgent:解决敏捷估计“黑箱+不协作”的终极方案
  • 鸿蒙Next ArkTS卡片开发指南:从入门到实战
  • 【绕过disable_function】
  • 使用云手机运行手游的注意事项有哪些?
  • 【数据结构】利用堆解决 TopK 问题
  • 2025陇剑杯现场检测
  • openharmony之充电空闲状态定制开发
  • 【开题答辩全过程】以 python的线上订餐系统为例,包含答辩的问题和答案
  • (附源码)基于Spring Boot的校园心理健康服务平台的设计与实现
  • 微信小程序开发教程(十八)