【数据结构】双向循环链表专题解析

实现自己既定的目标,必须能耐得住寂寞单干。💓💓💓

目录

•✨说在前面

🍋知识点一:双向链表的结构

 • 🌰1."哨兵位"节点

 • 🌰2.双向带头循环链表的结构

🍋知识点二:双向带头循环链表

 • 🌰1. 动态申请节点 

 • 🌰2. 双向链表的初始化

 • 🌰3. 双向链表元素的打印

 • 🌰4. 双向链表头部插入数据

 • 🌰5. 双向链表尾部插入数据

 • 🌰6. 指定位置pos之后插入数据

 • 🌰7.双向链表头部删除元素

 • 🌰8.双向链表尾部删除元素

 • 🌰9.删除指定位置pos节点

 • 🌰10.双向链表的查找

 • 🌰10.双向链表的销毁

• ✨SumUp结语


•✨说在前面

亲爱的读者们大家好!💖💖💖,我们又见面了,之前我们学习了顺序表后,又紧接着给大家讲解了链表中最典型的单向不循环链表,也是最常用的一种。但正所谓我们学习应该面面俱到,有了之前的学习基础,再学习双向链表实际上是非常简单的。

  

 如果你没有准备好的话,可以再复习一下单链表以及单链表相关LeetCode的OJ题。

   

👇👇👇
💘💘💘知识连线时刻(直接点击即可)

  🎉🎉🎉复习回顾🎉🎉🎉

【数据结构】单链表专题详细分析

    

  博主主页传送门:愿天垂怜的博客

 

 

🍋知识点一:双向链表的结构

 • 🌰1."哨兵位"节点

哨兵位指的是链表中指向链表第一个节点的节点,哨兵位不存储任何有效元素,只是在那里放哨的,顾称为哨兵位节点

注意:

这里的"带头"跟前面我们说的"链表中的第一个有效节点"是两个概念,实际单链表的头结点不是第一个有效节点,而是哨兵位节点。

"哨兵位"存在的意义:

遍历循环链表避免死循环。

具体带头比不带头有什么优势可以看我上一篇文章中的合并有序链表。

LeetCode/NowCoder-链表经典算法OJ练习1

 • 🌰2.双向带头循环链表的结构

结构如下:

 由于"哨兵位"节点的存在,我们再实现这样的链表时可以省去一些内容:

🎉插入操作时,不需要检查是否在头部插入,因为哨兵节点作为头结点,总是存在。

🎉删除操作时,不需要处理删除的是否是头节点的情况,因为哨兵节点不会被删除。

🎉简化了代码,因为不需要为头节点和普通节点编写不同的处理逻辑。

类比单链表的结构,可以定义出节点数据为整型的双向带头循环链表节点:

typedef int LTDataType;

typedef struct ListNode
{
	LTDataType data;
	struct ListNode* prev;//指向前一个节点
	struct ListNode* next;//指向后一个节点
}LTNode;

🍋知识点二:双向带头循环链表

 • 🌰1. 动态申请节点 

在双向带头循环链表提供的方法中,动态申请节点是必不可少的。

LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc");
		exit(1);
	}
	newnode->data = x;
	newnode->next = newnode->prev = newnode;
    return newnode;
}

初始化双向链表其实就是创建头节点,那么就需要用到LTBuyNode来申请节点。由于是双向的,我们可以让它的next指针和prev指针先指向它自己。

那是否可以将哨兵位的next指针和prev指针初始化为NULL呢?答案是不行的。

如果哨兵位的前驱指针prev和后继指针next初始化为NULL,这样的链表虽然满足了双向,也满足了带头,但是不满足循环,所以不能这样初始化,因此在动态申请节点的时候,就让newnode的prev和next都指向它自己,这样就可以循环起来了。

 • 🌰2. 双向链表的初始化

初始化双向带头循环链表实际上就是创建"哨兵位"头节点。

写法1:传二级指针,函数为void型。

void LTInit(LTNode** pphead)
{
	*pphead = LTBuyNode(-1);
}

写法2:不传参 ,在函数中创建节点,函数为LTNode*型。

LTNode* LTInit()
{
    LTNode* phead = LTBuyNode(-1);
	return phead;
}

我们将其中存储的值设为-1,表示其为头节点,此时链表中只有一个头节点:

 • 🌰3. 双向链表元素的打印

打印双向链表中的所有节点数据。

void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		printf("%d->", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

这个没什么好说的,和之前我们学习的单链表基本是一样的,不过是while循环的条件有所变化。我们如果初始化pcur为phead->next,那它循环完一轮之后会重新变成phead,此时就已经打印完成,不需要继续循环了。

 • 🌰4. 双向链表头部插入数据

向双向链表的头部插入节点和数据,指的是在头节点的后一个位置插入数据,而不是在头节点的前面(头插)。

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->prev = phead;
	newnode->next = phead->next;
	phead->next = newnode;
	newnode->next->prev = newnode;
}

双向带头循环链表的指针关系比较复杂,但是逻辑却很简单,我们一定要通过画图来理解,不要光看代码,这样是看不出东西的。

上述关系红色为先修改,蓝色为后修改。由于关系比较多,可以先从newnode入手,先处理newnode的prev和next指针,这样不会影响到原链表的结构。当设置完newnode时,如果先让phead的后继指针next指向newnode,那么图中红色所标注的phead->next不在是那个节点的地址,而是newnode的地址,此时它就应该是newnode->next。

 • 🌰5. 双向链表尾部插入数据

向双向链表的尾部插入节点和数据,可以理解为在图中的末尾插入数据,也可以理解为在头节点的前一个位置插入数据。

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	newnode->prev = phead->prev;
	newnode->next = phead;
	phead->prev->next = newnode;
	phead->prev = newnode;
}

由于是循环链表,画图的时候可以在节点顺序不改变的前提下灵活变换图像。

图1:

图2:


 两中图理解都是可以的,由于头节点的位置固定,上面两个图都是同一个双向链表。 

 • 🌰6. 指定位置pos之后插入数据

在指定位置pos的后面插入节点和数据,其实和头插方法很类似,甚至可以说头插其实就是pos后插入数据的一种特殊情况,只不过此时pos=phead而已。

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(x);
	newnode->prev = pos;
	newnode->next = pos->next;
	pos->next = newnode;
	newnode->next->prev = newnode;
}

可以发现,代码也是及其类似。

 • 🌰7.双向链表头部删除元素

删除链表中的第一个有效节点,即删除头节点的后一个节点。

void LTPopFront(LTNode* phead)
{
	assert(phead && phead->next != phead);
	LTNode* del = phead->next;
	phead->next = del->next;
	del->next->prev = phead;
	free(del);
	del = NULL;
}

为了防止free释放后无法解引用找到下一个节点的问题,引入del保存要删除的节点的地址。

 • 🌰8.双向链表尾部删除元素

 删除双向链表中的最后一个数据,可以理解为删除图中末尾的数据,也可以理解为删除头节点前面一个数据。

void LTPopBack(LTNode* phead)
{
	assert(phead && phead->next != phead);
	LTNode* del = phead->prev;
	phead->next = del->prev;
	del->prev->next = phead;
	free(del);
	del = NULL;
}

两种画法和2.5思路一样,这里就画出一种,只要理解一种,另一种就很简单了。

 • 🌰9.删除指定位置pos节点

删除指定位置pos的节点,如法炮制。

void LTErase(LTNode* pos)
{
	//pos不能为哨兵位
	assert(pos);
	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;
	free(pos);
	pos = NULL;
}

除了要保证pos不为NULL外,还需要保证pos不能为哨兵位,否则它不是有效节点。

 • 🌰10.双向链表的查找

查找链表中值为x的节点。

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

定义pcur,利用while循环遍历一轮,如果有值为x的节点,则返回该节点,遍历一轮后若仍没有该节点,则返回空指针NULL。

 • 🌰10.双向链表的销毁

与单链表和顺序表的思想如出一辙,如法炮制。

void LTDestory(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(phead);
	phead = NULL;
}

• ✨SumUp结语

数据结构的学习一定要多画图,多理解,多思考,切忌直接抄写代码,就认为自己已经会了,一定到自己动手,才能明白自己哪个地方有问题。

如果大家觉得有帮助,麻烦大家点点赞,如果有错误的地方也欢迎大家指出~

  • 40
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 18
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值