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

news2024/9/21 20:43:50

       

目录

1、双向循环链表的结构

2、双向循环链表的结构体创建

3、双向循环链表的初始化

3.1 双向链表的打印

 4、双向循环链表的头插

 5、双向循环链表的尾插

6、双向循环链表的删除

6.1 尾删

6.2 头删

6.3 小节结论        

7、查找

8、在pos位置前插入数据

9、删除pos位置的数据

10、释放双向循环链表

结语:


前言:

         双向循环链表在实际应用中是一种非常广泛的数据结构,双向循环链表在结构上比单链表更复杂,比如单链表中的节点只有一个指针,而双向循环链表中有两个指针共同维护该节点,其中一个指针指向后一个节点,而另一个指针指向前面一个节点。虽然其结构较复杂,但是在实现增删查改的功能上却比单链表要便捷的多。

1、双向循环链表的结构

        上图的第一个节点head称为该链表的头节点(也称为哨兵位的节点,他的作用就好比一个哨兵只负责站岗),该节点中与其他节点的结构一样,只是该头节点中的数据不具有有效性,也就是打印数据时除了头节点的数据不打印,其他节点的数据都要打印。 

        该头节点的优势在于无需更改pilst的指向,因为plist指针始终指向该节点,只需要对该节点内部成员的指针进行修改即可,同时也可以简化代码,具体如下文。

2、双向循环链表的结构体创建

        节点的结构体代码如下:

typedef int DListDataType;//int类型重定义,方便使用其他类型时进行修改

typedef struct DoubleListNode
{
	struct DoubleListNode* prev;//prev指向前一个节点的指针
	struct DoubleListNode* next;//next指向后一个节点的指针
	DListDataType data;//存储的数据
}DLNode;//重定义结构体类型

3、双向循环链表的初始化

       初始化即生成一个头节点,即哨兵位节点,并且用一个指针指向其节点即可。因为是循环链表,因此初始化的时候要将头节点的两个指针都指向自己。

         因此当链表为空的时候,头节点(哨兵位节点)依然是存在的,则在对链表进行删除操作时头节点不能被删除。 

        初始化代码如下:

DLNode* DLInit()//初始化
{
	DLNode* phead = BuyNode(-1);//节点创建函数

	phead->next = phead;//只有一个头节点的时候也是自己指向自己
	phead->prev = phead;

	return phead;//返回头节点的地址
}

int main()
{
    DLNode* plist = DLInit();//外部有一个结构体指针来接收返回值
    return 0;
}

3.1 双向链表的打印

        打印双向链表,以便观察各个功能的结果:

void Print(DLNode* phead)//打印
{
	assert(phead);

	DLNode* cur = phead->next;
	printf("哨兵位<=>");
	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
}

 4、双向循环链表的头插

         思路很简单,就是将newnode节点的next指向d1,并且d1的prev指向newnode。newnode的prev指向head,head的next指向newnode。但是这里涉及到节点的创建,每次插入节点的时候都需要创建节点,因此将其封装成一个函数如下:

DLNode* BuyNode(DListDataType x)
{
	DLNode* newnode = (DLNode*)malloc(sizeof(DLNode));//malloc开辟空间
	if (newnode == NULL)//判断malloc是否成功
	{
		perror("BuyNode");
		return NULL;
	}

	newnode->data = x;//赋予创建节点的值
	newnode->prev = NULL;//置空
	newnode->next = NULL;//置空

	return newnode;//返回节点的地址
}

        头插代码: 

void PushFront(DLNode* phead, DListDataType x)//头插
{
	assert(phead);

	DLNode* next = phead->next;//定义一个指针next,他指向phead的下一个节点
	DLNode* newnode = BuyNode(x);//创建节点

	phead->next = newnode;//phead下一个节点为newnode
	newnode->prev = phead;//newnode前一个节点为phead
	newnode->next = next;//newnode下一个节点为next
	next->prev = newnode;//next前一个节点为newnode
}

 5、双向循环链表的尾插

        思路与头插差不多,但是尾插能体现出双向循环链表的优势在于无需遍历整个链表去找尾,因为head的prve指向的就是尾, 因此可以直接找到尾,然后再进行尾插操作。

        尾插代码如下:

void PushBack(DLNode* phead, DListDataType x)//尾插
{
	assert(phead);

	DLNode* tail = phead->prev;//找到尾部节点
	DLNode* newnode = BuyNode(x);//创建节点

	tail->next = newnode;//让尾部的下一个节点为newnode
	newnode->prev = tail;//让newnode的上一个节点为tail
	phead->prev = newnode;//让phead的上一个节点为newnode
	newnode->next = phead;//让newnode下一个节点是phead
}

6、双向循环链表的删除

6.1 尾删

         思路:从上文可以得知,找到尾节点很简单,但是这里需要再定义一个指针,他指向tail前一个节点。然后将tail释放后,再把tailprev的next指向头节点,头节点的prev更新成指向tailprev节点即可完成尾删。

        尾删代码如下:

bool Empty(DLNode* phead)//判断链表是否为空
{
	assert(phead);

	return phead->next == phead;//为空返回真
}

void PopBack(DLNode* phead)//尾删
{
	assert(phead);
	assert(!Empty(phead));//若返回真表示链表为空,则结果取反,断言不通过

	DLNode* tail = phead->prev;//定义tail指针
	DLNode* tailPrev = tail->prev;//定义tailPrev指针

	tailPrev->next = phead;//tailPrev下一个节点为phead
	phead->prev = tailPrev;//让phead的前一个节点为tailPrev
	free(tail);//释放tail节点
}

6.2 头删

         思路:将头节点与second指向的节点互相联系起来,然后将first释放即可。

        头删代码如下:

void PopFront(DLNode* phead)//头删
{
	assert(phead);
	assert(!Empty(phead));//链表判空

	DLNode* first = phead->next;//定义指针
	DLNode* second = first->next;

	phead->next = second;//将头节点于second节点相连
	second->prev = phead;
	free(first);//删除first节点
}

6.3 小节结论        

        这里可以体现出双向循环链表相对于单链表的一个优势:链表只有一个节点的时候,直接删除即可无需将plist指针置为空,因为plist指针始终指向哨兵节点。若是单链表进行删除则还要进行多一项的判断,还要考虑plist指针是否为空的情况。

        但是其缺陷是空链表的时候若还进行删除会把哨兵位也删了,这时候plist就是野指针,对pilst进行解引用就会出现非法访问的问题。因此要断言链表是否为空,为空则不能进行删除。 

7、查找

        查找功能就相对简单点,只需要遍历链表,返回要查找节点的地址pos即可,只是这里需要注意一点,即:遍历链表的条件,因为头节点的数据没有有效性,因此从头节点的下一个节点开始遍历,直到遍历到头节点结束。

        查找代码如下:

DLNode* Seach(DLNode* phead, DListDataType x)//搜索
{
	assert(phead);

	DLNode* cur = phead->next;//用cur代替phead去遍历
	while (cur != phead)//cur指向头节点时结束循环
	{
		if (cur->data == x)
			return cur;//如果等于返回cur地址
		cur = cur->next;//遍历cur
	}
	return NULL;//链表中没有该数据则返回空
}

        查找函数通常与中间插入、中间删除函数进行搭配,因为查找函数返回了一个地址pos,再将这个地址直接传到中间插入、中间删除函数内,就能很好的实现功能。

8、在pos位置前插入数据

         思路:通过查找函数可以得到pos位置的地址,然后再定义一个指针prev,再将newnode节点与prev节点和pos节点联系起来即可。

        pos前插代码如下:

void PosInsert(DLNode* pos, DListDataType x)//在pos前插入
{
	assert(pos);

	DLNode* posPrev = pos->prev;//定义posprev指针,指向pos前面一个节点
	DLNode* newnode = BuyNode(x);//创建节点

	newnode->prev = posPrev;//以下的操作就是把newnode节点与pos、posprev节点联系起来
	posPrev->next = newnode;
	newnode->next = pos;
	pos->prev = newnode;
}

        插入数据与查找数据搭配测试代码如下,随便测试之前的接口:

int main()
{
	DLNode* plist = DLInit();//初始化
	PushFront(plist, 4);//头插
	PushFront(plist, 3);
	PushFront(plist, 2);
	PushFront(plist, 1);
	PushBack(plist, 5);//尾插
	PushBack(plist, 6);

	/*PopFront(plist);
	PopBack(plist);*/

	Print(plist);//打印

	DLNode* pos = Seach(plist, 1);//查找
	if (pos)
	{
		PosInsert(pos, 20);//中间插入
	}
	printf("\n");//换行
	Print(plist);//打印

    DLNodeFree(plist);//释放链表
	plist = NULL;//手动将plist置空
	return 0;
}

        运行结果:

9、删除pos位置的数据

        思路:将posPrev与posNext这两个节点联系起来,然后释放pos即可。

        删除pos位置代码如下:

void PosDestroy(DLNode* pos)//删除pos位置
{
	assert(pos);

	DLNode* posPrev = pos->prev;//定义两个指针,一个指向pos前面节点,一个指向pos后面节点
	DLNode* posNext = pos->next;

	posPrev->next = posNext;//将这两个指针指向的节点联系在一起
	posNext->prev = posPrev;
	free(pos);//释放pos节点
}

10、释放双向循环链表

        因为链表中的各个节点(包括哨兵位节点)都是在堆上申请的,因此在使用完毕后应该对这些空间进行释放。

        释放函数代码如下:

void DLNodeFree(DLNode* phead)//释放链表
{
	assert(phead);//此处要断言,不然下面会对空指针解引用

	DLNode* cur = phead->next;//用cur指针代替phead去遍历
	
	while (cur != phead)
	{
        //Next指针的作用是记住位置,防止释放cur后找不到下一个节点
		DLNode* Next = cur->next;
		free(cur);//释放cur指向的节点
		cur = Next;//赋予cur新的位置
	}

	free(phead);//最后释放哨兵位(头节点)
}

int main()
{
	DLNode* plist = DLInit();

	DLNodeFree(plist);
	plist = NULL;//外部的plist此时的野指针,要手动置空
	return 0;
}

        这里注意的点:若在DLNodeFree函数内部进行对phead的置空,则不会影响外部plist的值,因为phead只是plist的一份临时拷贝,除非将pilst的地址传给DLNodeFree函数,用二级指针来操作,或者手动在外面将plist置空,两种方法都能将plist置空。

结语:

        以上就是关于双向循环链表的实现与解析,如果本文对你起到了帮助,希望可以点赞👍+关注😎+收藏👌哦!如果有遗漏或者有误的地方欢迎大家在评论区补充~!!谢谢大家!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1202558.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Scala---介绍及安装使用

一、Scala介绍 1. 为什么学习Scala语言 Scala是基于JVM的语言&#xff0c;与java语言类似&#xff0c;Java语言是基于JVM的面向对象的语言。Scala也是基于JVM&#xff0c;同时支持面向对象和面向函数的编程语言。这里学习Scala语言的原因是后期我们会学习一个优秀的计算框架S…

单链表(7)

插入函数——插入数据&#xff0c;在链表plist的pos位置插入val数据元素 由图知&#xff0c;poslength时&#xff0c;是可以插入的 在大多数情况下&#xff0c;说位置的时候&#xff0c;从0开始计数&#xff1b;说第几个数据的时候&#xff0c;从1开始计数 现在来测试一下 这就…

CSDN的规范、检测文章质量、博客等级好处等等(我也是意外发现的,我相信很多人还不知道,使用分享给大家!)

前言 都是整理官方的文档&#xff0c;方便自己查看和检查使用&#xff0c;以前我也不知道。后来巧合下发现的&#xff0c;所以分享给大家&#xff01; 下面都有官方的链接&#xff0c;详情去看官方的文档。 大家严格按照官方的规范去记录自己工作生活中的文章&#xff0c;很快…

MacOS Ventura 13 优化配置(ARM架构新手向导)

一、系统配置 1、About My MacBook Pro 2、在当前标签打开新窗口 桌面上创建目录的文件夹&#xff0c;每次新打开一个目录&#xff0c;就会创建一个窗口&#xff0c;这就造成窗口太多&#xff0c;不太好查看和管理&#xff0c;我们可以改成在新标签处打开新目录。需要在&…

电动自动换刀高速电主轴的技术优势浅析

在制造业中&#xff0c;自动化技术的发展一直是一个重要的话题。其中&#xff0c;电动自动换刀被认为是一项高效、智能、先进的技术&#xff0c;在高速电主轴中使用电动自动换刀这一技术&#xff0c;不仅能够缩短换刀时间&#xff0c;还能减少换刀失误&#xff0c;本文将探讨Sy…

光计算1周2篇Nature,英伟达的时代彻底结束!

近期&#xff0c;光计算领域连续发出重量级文章&#xff0c;刊登在学术界的顶级期刊上。一时间&#xff0c;各大媒体纷纷转发&#xff0c;读者们也纷纷感叹&#xff1a;中国芯片取代英伟达的机会来了&#xff01;今天&#xff0c;光子盒用这篇万字长文为大家梳理光计算的背景、…

指标类型(一):北极星指标、虚荣指标

每个产品都有很多指标&#xff0c;每个指标都反映了对应业务的经营情况。但是在实际业务经营中&#xff0c;却要求我们在不同的产品阶段寻找到合适的指标&#xff0c;让这个指标可以代表当前产品阶段的方向和目标&#xff0c;让这个指标不仅对业务经营团队&#xff0c;而且对产…

双十一网络电视盒子哪个品牌好?内行分享权威电视盒子排行榜

双十一大促正如火如荼进行中&#xff0c;因为我从事的工作和电视盒子有关&#xff0c;身边的朋友们在选购电视盒子时不知道从何下手就会问我的意见&#xff0c;本期将盘点业内公认的电视盒子排行榜&#xff0c;给双十一想买电视盒子的朋友们做个参考。 排行一&#xff1a;泰捷W…

【C++】非类型模板参数 | array容器 | 模板特化 | 模板为什么不能分离编译

目录 一、非类型模板参数 二、array容器 三、模板特化 为什么要对模板进行特化 函数模板特化 补充一个问题 类模板特化 全特化与偏特化 全特化 偏特化 四、模板为什么不能分离编译 为什么 怎么办 五、总结模板的优缺点 一、非类型模板参数 模板参数分两类&#x…

MVVM框架:图片加载有问题

一、前言&#xff1a;在我使用ImageView加载图片的时候添加如下代码发现报错 app:imageUrl"{viewModel.observableField.assetImg}"报错如下错误 二、原因&#xff1a;是啥我不太清楚好像是没有imageView的适配器&#xff0c;后来我看了一下确实没有 public class I…

Java中所有的运算符,以及运算符优先级(总结)

运算法是一种特殊的符号&#xff0c;用于表示数据的运算、复制、比较等。 1、算数运算符 // % 取余运算&#xff1a;结果的符号和被模数的符号一致 12 % 5 2 -12 % 5 -2 12 % -5 2 -12 % -5 -2int a1 10; int b1 a1; // a111, b111 int a2 10; int b2 a2; // a211, …

keras转onnx,TensorFlow转tf.keras.models.load_model,onnx精度转换

参考&#xff1a; https://blog.csdn.net/Deaohst/article/details/126864267 转onnx 别直接转onnx。 先转PB&#xff1a; import tensorflow as tfmodel_path ./models/model.h5 # 模型文件 model tf.keras.models.load_model(model_path) model.sa…

Microsoft outlook已停止工作

故障现象 运行outlook&#xff0c;无法进入程序,显示已停止工作 故障截图 解决方案 1、点击查看问题详细信息&#xff0c;查看故障模块名称&#xff0c;一般是某个dll动态链文件&#xff0c;然后下载或者拷贝该dll文件将其覆盖即可 2、如果找不到&#xff0c;可以使用专用的…

如何向MapInfo Pro添加自定义符号?

用户可以在MapInfo Pro中创建和使用自己的自定义图像作为符号。要访问这些自定义符号&#xff0c;请将它们放在CUSTSYMB目录中&#xff0c;然后从“符号样式”对话框&#xff08;Style>符号样式&#xff09;的“字体”列表中的“自定义符号”选项中选择它们。MapInfo Pro中的…

智慧化城市内涝的预警,万宾科技内涝积水监测仪

随着城市化进程的加速&#xff0c;伴随的是城市内涝问题日益凸显。频繁的暴雨和积水给市民的生活带来了诸多不便&#xff0c;也给城市的基础设施带来了巨大压力。如何解决这一问题&#xff0c;成为智慧城市建设的重要课题和政府管理的工作主题&#xff0c;只要内涝问题得到缓解…

实时数仓-Flink使用总结

阿里云实时计算Flink版是阿里云基于Apache Flink构建的企业级、高性能实时大数据处理系统。具备一站式开发运维管理平台&#xff0c;支持作业开发、数据调试、运行与监控、自动调优、智能诊断等全生命周期能力。本期将对Flink的使用进行总结。 1. Flink产品回顾 阿里云实时计算…

11月13日星期一今日早报简报微语报早读

11月13日星期一&#xff0c;农历十月初一&#xff0c;早报微语早读。 1、国家邮政局&#xff1a;“双11”当天全国快递业务量达6.39亿件&#xff1b; 2、公安机关通缉4名缅北电诈头目&#xff0c;其中一人为缅甸掸邦议会原议员&#xff1b; 3、多部门提醒&#xff1a;未满10…

【Debug】此语法需要一个导入的帮助程序,但找不到模块“tslib”

报错如下&#xff1a; 代码可以编译运行&#xff0c;但是会有红线和报错。 解决方法&#xff1a;TypeScript error “TS2354: This syntax requires an imported helper but module ‘tslib’ cannot be found” Issue #37991 microsoft/TypeScript GitHub 在tsconfig.jso…

MacOS下VMware Fusion配置静态IP

前言 在虚拟机安装系统后&#xff0c;默认是通过DHCP动态分配的IP&#xff0c;这会导致每次重启虚拟机ip都可能会改变&#xff0c;使用起来会有很多不便。 配置静态IP 查看主机网关地址 cat /Library/Preferences/VMware\ Fusion/vmnet8/nat.conf 查看主机DNS&#xff0c;m…

【中间件篇-Redis缓存数据库06】Redis主从复制/哨兵 高并发高可用

Redis高并发高可用 复制 在分布式系统中为了解决单点问题&#xff0c;通常会把数据复制多个副本部署到其他机器&#xff0c;满足故障恢复和负载均衡等需求。Redis也是如此&#xff0c;它为我们提供了复制功能&#xff0c;实现了相同数据的多个Redis 副本。复制功能是高可用Re…