【数据结构】链表:带头双向循环链表的增删查改

这篇具有很好参考价值的文章主要介绍了【数据结构】链表:带头双向循环链表的增删查改。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

本篇要分享的内容是带头双向链表,以下为本片目录

目录

一、链表的所有结构

二、带头双向链表

2.1尾部插入

2.2哨兵位的初始化

2.3头部插入

2.4 打印链表

2.5尾部删除

2.6头部删除

 2.7查找结点

2.8任意位置插入

2.9任意位置删除 


在刚开始接触链表的时候,我们所学仅仅所学的是单链表,相信大家用C语言学习单链表时也倍受二级指针的折磨。当然单链表只是链表结构内的一种,他的结构非常简单,但是理解并操作起来却非常困难;而我们今天要研究的是链表中结构最复杂,但是理解起来最简单的链表的结构。

一、链表的所有结构

在学习带头双向链表之前先了解一下链表的所有结构

1.单向或双向

【数据结构】链表:带头双向循环链表的增删查改

 2.带头或不带头

【数据结构】链表:带头双向循环链表的增删查改

 3.循环或不循环

 还可以将以上的链表结构进行组合

【数据结构】链表:带头双向循环链表的增删查改

最终链表有八种结构。

这里要说明的是带头和不带头的情况,这里的头意思就是哨兵位,哨兵位也就是作为链表的开头链接后面的数据,但是不存放任何数据,需要单独开辟一个结点来确定哨兵位,这就是带头不带头的意思。

二、带头双向链表

其实我们也没有必要一个一个的去了解那么多的链表结构,我们平时用到的最多的还是两个结构

【数据结构】链表:带头双向循环链表的增删查改

 1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结
构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都
是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带
来很多优势,实现反而简单了,后面我们代码实现了就知道了。

2.1尾部插入

和单链表相同,我们同样掌握带头循环链表的增删查改,但是带头循环链表的增删查改会比单链表容易许多。

首先定义一个结构体来存放前一个结点的位置和后一个结点的位置和数据;

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* prev;
	struct ListNode* next;
	LTDataType Data;
}LTNode;

接下来画图给大家演示一下

【数据结构】链表:带头双向循环链表的增删查改

【数据结构】链表:带头双向循环链表的增删查改

 这里要插入的是newnode这个结点,我们可以直接操作phead的prev来实现尾插。

和单链表的尾插相同的,尾插就得先找尾,这个链表结构中的找尾,只需要通过phead的prev即可找到尾,比单链表中的找尾要方便许多。

插入newnode也非常简单,只需要改变四个指针的位置即可,以下是尾部插入的代码

void PushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* tail = phead->prev;
	LTNode* newnode = BuyLTNode(x);
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = phead;
	phead->prev = newnode;
}

在对比一下以上两幅图和尾插的代码,首先写出一个开辟节点的代码用来开辟newnode(放在后面说)。

newnode已经开辟好,然后改变四个指针的方向;

先让哨兵位phead通过prev来找尾,令尾为tail;

让tail的next来指向新的结点newnode;

再让新结点newnode的prev链接上一个结点tail;

再让新节点newnode的next重新指向哨兵位;

最后让哨兵位phead的prev指向新节点newnode;这样newnode才能成为链表的尾结点;

这样就是一个完整的尾插;

兄弟萌,对比单链表的尾插,在逻辑上带头循环链表的尾插是要简单一些,一目了然。

2.2哨兵位的初始化

那为什么可以这么这么简单呢?

如下图

【数据结构】链表:带头双向循环链表的增删查改

可以看到我们对哨兵位的初始化,如果链表为空时,phead的prev就指向自己,phead的next也可以指向自己,这就是对哨兵位的初始化

LTNode* LTInit()
{
	LTNode* phead = BuyLTNode(-1);
	phead->next = phead;
	phead->prev = phead;
	return phead;
}

2.3头部插入

既然都是插入我们不妨大胆猜测一下,他是否和尾插一样呢?

接下来继续画图分析:

【数据结构】链表:带头双向循环链表的增删查改头插要注意的是要插入到哨兵位之后,因为我们需要通过哨兵位来找到这个链表,所以要在哨兵位后面插入,也就是头插。

同样的上图先malloc了一个结点出来,那如何处理这个结点呢?

假设我们和尾插一样,先处理这个结点前面的指针

【数据结构】链表:带头双向循环链表的增删查改让phead的next指向newnode;

让newnode的prev指向phead;

再让newnode的next指向下一个结点;

这样做真的可以做到吗?

 当然是不行的啦;

你有没有发现,如果我们先改变phead的next,令他指向newnode的话,它还可以找到newnode的下一个结点吗?显然是不行的,所以在头部插入时我们需要先改变newnode后面的结点,让newnode先和后面的结点链接起来,再让他和前面的结点进行连接,这样才是头插的正确用法。

下面是头插的代码

void PushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode= BuyLTNode(x);

	newnode->next = phead->next;
	phead->next->prev = newnode;
	phead->next = newnode;
	newnode->prev = phead;

}

可以对照上图仔细阅读一下代码,应该不难看懂;

2.4 打印链表

既然我们上面讨论过头插和尾插,我们不妨将其输出验证一下结果

void LTPrint(LTNode* phead)
{
	assert(phead);
	printf("sentinel bit<==>");
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<==>", cur->Data);
		cur = cur->next;
	}
	printf("\n");
}

sentinel是哨兵位的意思,我们要通过哨兵位才能找到这些链表;

我们先定义一个新的结构体指针cur,让cur指向头节点的下一个结点,向后迭代,通过cur来遍历这个链表,简单来说就是从哨兵位后面的那个结点开始遍历,当这个结点知道下一个是哨兵位的时候结束了。

那结合上面的图示我们可以知道phead的next走到最后就会又回到phead的位置,所以我们不妨让cur=phead成为循环结束的标志,当cur!=phead 的时候就打印链表内容

我们应用上面的两个插入函数来验证

【数据结构】链表:带头双向循环链表的增删查改

 可以看到我们的头部插入和尾部插入,还有打印函数都非常的成功。

2.5尾部删除

我们不妨先看看单链表的尾部删除

【数据结构】链表:带头双向循环链表的增删查改

 可以看到步骤是相当的繁琐,因为不仅要找尾,还要判空,还要判断是否只有一个数据,非常非常的麻烦,可以说是集各种缺点于一身。

但是带头双向链表的尾部删除写起来非常爽

再继续画图来理解

【数据结构】链表:带头双向循环链表的增删查改

 带头双向链表的尾删只要通过phead的prev就可以找到尾结点tail,并且找到尾结点tail后可以继续通过tail->prev来找到tail的前一个结点,我们将他成为tailPrev

然后将phead的prev指向tailPrev这个结点;

再将tailPrev这个结点的next指向phead哨兵位,这样就把tail孤立出去了,此时tailPrev就成为了新的尾结点

最后再将tail用free释放掉就就可以达到尾删的结果

以下是代码

void PopBack(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	LTNode* tail = phead->prev;
	LTNode* tailprev = tail->prev;
	free(tail);

	tailprev->next = phead;
	phead->prev = tailprev;
}

可以对照着上面的图和文字步骤仔细理解一下代码的内容,应该不难看懂。

2.6头部删除

既然是头部删除,那就继续要在哨兵位上动手脚,我们继续来画图理解

【数据结构】链表:带头双向循环链表的增删查改

 相信这个图也很清晰了,通过哨兵位phead找到下一个结点的next,也就是next的next,然后free掉next来达到删除的效果,再让原先next的next的prev来指向哨兵位,这样第二个结点就代替了第一个结点达到头部删除的效果,下面是代码

void PopFront(LTNode* phead)
{
	assert(phead);
	assert(!LTEmpty(phead));
	LTNode* first = phead->next;
	LTNode* second = first->next;

	phead->next = second;
	second->prev = phead;

	free(first);

}

我们不妨将phead->next定义为first,将next->next定义位second,这样代码的可读性就会大大提高,将代码对照以上文字描述和图片仔细理解,应该不难看懂。

当然在增强代码可读性方面还需要做的一点就是assert的断言;可以看到上面的代码中出现了

assert(!LTEmpty(phead));

这样一串代码中assert怎么断言一个函数呢?那这是什么意思呢?

我们用bool写一个函数

bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;
}

意思就是说如果链表中没有元素了,phead的next还是等于phead的话就说明链表已经空了,这样做的好处就是可以提醒你链表中已经没有元素来让你删除了,从而达到暴力检查让编译器报错的效果。

我们在主函数中使用以下我们上面所写的删除函数

【数据结构】链表:带头双向循环链表的增删查改

 可以看到我们的尾部删除已经删掉了尾部插入的4,那我们再多次使用删除函数会怎样

【数据结构】链表:带头双向循环链表的增删查改

 可以看到再main函数中直插入了四个数据,但是却使用了五次删除函数,运行时就会报错,而报错的内容就是我们刚刚写的布尔函数,它可以大大加强代码的可读性。

 2.7查找结点

查找结点再带头双向循环列表中也不困难,同样的是要对链表进行遍历查找。我们要查找的是结点,所以定义函数类型的时候一定是结构体指针类型,找到了就返回他的结点,找不到就返回空,这就是大体思路

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

}

这里就需要传两个参数了,一个是链表的头节点以便于我们找到链表并遍历查找,另一个就是我们想要找的数x。

当然也需要重新定一个结构体指针来遍历数组,并且和打印函数的循环条件相同,当检测到下一个结点时哨兵位phead的时候就停止循环了,因为下一个结点是phead的时候已经遍历完整个链表了。

这时就要操作结构体中的数据Data了,如果遍历时结点中的数据Data等于我们传入的参数x,那么就烦回这个结点,如果没有找到就返回空。

其实查找函数可以和其他的函数嵌套使用,因为查找函数返回的是结构体指针类型,而其它函数的参数也是结构体指针类型,我们可以将其和插入函数和删除函数一起使用。

2.8任意位置插入

和其他的插入方法一样的只需要改变指针的指向的内容即可。

以下是图例

【数据结构】链表:带头双向循环链表的增删查改

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* prev = pos->prev;
	LTNode* newnode = BuyLTNode(x);

	prev->next = newnode;
	newnode->prev = prev;

	newnode->next = pos;
	pos->prev = newnode;
}

中间插入就不需要再使用phead了,因为phead是哨兵位,而中间插入需要的是其他的结点,也就是通过刚刚讨论过的查找函数所查找出来的结点,你就会发现,查找函数和其他函数就这样连接在一起了。

首先定义一个结构体指针prev来存放查找出来的那个结点的前面一个结点;

然后开辟一个新的结点newnode;

然后就和尾插一模一样的方法改变指针指向的内容即可。

也可以在main函数中使用验证一下

【数据结构】链表:带头双向循环链表的增删查改

这里的意思就是通过查找函数找到3的位置,并且再3的前面插入30,验证正确;

2.9任意位置删除 

同样的也需要通过查找函数来确定删除的位置,以下是图例

【数据结构】链表:带头双向循环链表的增删查改

 仔细研究完头部删除和尾部删除的内容应该不难看懂。

首先要找到pos前面的一个结点和后面的一个结点,然后直接将前一个结点的next指向pos的下一个结点;

将pos的下一个结点的prev指向pos的上一个结点;

最后再free掉pos这个位置即可完成删除操作;

以下是任意位置删除的代码

void LTErase(LTNode* pos)
{
	assert(pos);
	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;

	posPrev->next = posNext;
	posNext->prev = posPrev;
	free(pos);
	;
}

 继续再mian函数中使用 

【数据结构】链表:带头双向循环链表的增删查改可以看到3就被删除掉了。

以上就是带头双向循环链表增删查改使用的所有内容,如果对你有所帮助还请多多三联支持,感谢您的阅读。文章来源地址https://www.toymoban.com/news/detail-453483.html

到了这里,关于【数据结构】链表:带头双向循环链表的增删查改的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • 【数据结构 -- C语言】 双向带头循环链表的实现

    【数据结构 -- C语言】 双向带头循环链表的实现

    目录 1、双向带头循环链表的介绍 2、双向带头循环链表的接口 3、接口实现 3.1 开辟结点 3.2 创建返回链表的头结点 3.3 判断链表是否为空 3.4 打印 3.5 双向链表查找 3.6 双向链表在pos的前面进行插入 3.6.1 头插 3.6.2 尾插 3.6.3 更新头插、尾插写法 3.7 双向链表删除pos位置的节点

    2024年02月09日
    浏览(45)
  • 数据结构 - 链表详解(二)—— 带头双向循环链表

    数据结构 - 链表详解(二)—— 带头双向循环链表

    链表的结构一共有 八种 :带头单向循环链表、带头单向非循环链表、带头双向循环链表、带头双向非循环链表、无头单向循环链表、无头单向非循环链表、无头双向循环链表、无头双向非循环链表。 今天我们来详解带头双向循环链表 带头双向循环链表是一种数据结构,它通

    2024年04月26日
    浏览(10)
  • 【数据结构和算法】实现带头双向循环链表(最复杂的链表)

    【数据结构和算法】实现带头双向循环链表(最复杂的链表)

    前文,我们实现了认识了链表这一结构,并实现了无头单向非循环链表,接下来我们实现另一种常用的链表结构,带头双向循环链表。如有仍不了解单向链表的,请看这一篇文章(7条消息) 【数据结构和算法】认识线性表中的链表,并实现单向链表_小王学代码的博客-CSDN博客

    2024年01月17日
    浏览(14)
  • 链接未来:深入理解链表数据结构(二.c语言实现带头双向循环链表)

    链接未来:深入理解链表数据结构(二.c语言实现带头双向循环链表)

    上篇文章简述讲解了链表的基本概念并且实现了无头单向不循环链表:链接未来:深入理解链表数据结构(一.c语言实现无头单向非循环链表)-CSDN博客 那今天接着给大家带来带头双向循环链表的实现 : 头文件DoubleList.h:用来基础准备(常量定义,typedef),链表表的基本框架

    2024年01月16日
    浏览(39)
  • 数据结构课程设计题目——链表综合算法设计、带头双向循环链表、插入、显示、删除、修改、排序

    数据结构课程设计题目——链表综合算法设计、带头双向循环链表、插入、显示、删除、修改、排序

      课程设计题目1–链表综合算法设计   一、设计内容   已知简单的人事信息系统中职工记录包含职工编号(no)、职工姓名(name)、部门名称(depname)、职称(title)和工资数(salary)等信息(可以增加其他信息),设计并完成一个简单的人事信息管理系统,要求完成但不

    2024年02月08日
    浏览(16)
  • 链表的极致——带头双向循环链表

    链表的极致——带头双向循环链表

    ​ 双向带头循环链表是链表结构最复杂,但使用最方便的链表。 [图中phead表示链表的头节点(哨兵); d1,d2等表示链表存储的数据; d1,d2等左侧的方框存储是指向该节点的上一个节点的指针(prev),右侧方框存储指向该节点的下一个的指针(next)] 双向带头循环链表的每

    2024年04月10日
    浏览(11)
  • 数据结构_链表_双向循环链表的初始化、插入、删除、修改、查询打印(基于C语言实现)

    数据结构_链表_双向循环链表的初始化、插入、删除、修改、查询打印(基于C语言实现)

    版本: 2024年4月26日 V1.0 发布于博客园 目录 目录 双向循环链表公式 初始化双向循环链表 构建双向循环链表结点 创建一个空链表(仅头结点) 创建一个新结点 插入数据 头插 中插 尾插 删除数据 头删 中删 尾删 查询打印数据 遍历打印 测试 测试结果: 完整代码 DoubleCirLList.h

    2024年04月27日
    浏览(19)
  • 带头双向循环链表--数据结构

    带头双向循环链表--数据结构

    😶‍🌫️😶‍🌫️😶‍🌫️😶‍🌫️Take your time ! 😶‍🌫️😶‍🌫️😶‍🌫️😶‍🌫️ 💥个人主页:🔥🔥🔥大魔王🔥🔥🔥 💥所属专栏:🔥魔王的修炼之路–数据结构🔥 如果你觉得这篇文章对你有帮助,请在文章结尾处留下你的 点赞 👍和 关注 💖,支持一

    2024年02月01日
    浏览(32)
  • 数据结构-带头双向循环链表

    数据结构-带头双向循环链表

    前言: 链表有很多种,上一章结,我复盘了单链表,这一章节,主要针对双链表的知识点进行,整理复盘,如果将链表分类的话,有很多种,我就学习的方向考察的重点,主要针对这两种链表进行整理。 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用

    2023年04月09日
    浏览(19)
  • 【数据结构】带头双向循环链表

    【数据结构】带头双向循环链表

      🧑‍🎓 个人主页:简 料   🏆 所属专栏:C++   🏆 个人社区:越努力越幸运社区   🏆 简       介: 简料简料,简单有料~在校大学生一枚,专注C/C++/GO的干货分享,立志成为您的好帮手 ~ C/C++学习路线 (点击解锁) ❤️ C语言阶段(已结束) ❤️ 数据结构与算法(ing) ❤

    2024年01月16日
    浏览(16)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包