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

前言:

👉 为什么选择双向链表?

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

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

一、🛠️双向链表的结构

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 目标节点,防止内存泄漏。

👉销毁:

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

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

相关推荐
aluluka2 小时前
Emacs 折腾日记(三十)——打造C++ IDE 续
c++·ide·emacs
今后1232 小时前
【数据结构】冒泡、选择、插入、希尔排序的实现
数据结构·算法·排序算法
半桔2 小时前
【网络编程】UDP 编程实战:从套接字到聊天室多场景项目构建
linux·网络·c++·网络协议·udp
Lucis__2 小时前
C++相关概念与语法基础——C基础上的改进与优化
c语言·开发语言·c++
草莓熊Lotso2 小时前
《算法闯关指南:优选算法--滑动窗口》--14找到字符串中所有字母异位词
java·linux·开发语言·c++·算法·java-ee
hhhhhshiyishi2 小时前
WLB公司内推|招联金融2026届校招|18薪
java·算法·机器学习·金融·求职招聘
---学无止境---3 小时前
九、内核数据结构之list
linux·数据结构·list
奔跑吧邓邓子3 小时前
【C++实战㊳】C++单例模式:从理论到实战的深度剖析
c++·单例模式·实战