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

       

目录

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置空。

结语:

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

机器人仿真GAZEBO开源代码分享

1、https://github.com/PRBonn/agribot 2、https://github.com/ros-mobile-robots/diffbot

腾讯待办为什么停止运营?ics文件如何导入日程APP继续使用?

有不少网友表示自己想要记录待办事项、设置待办提醒的时候&#xff0c;会直接使用微信中的腾讯待办小程序来记录。但是最近这段时间在使用这款小程序设置待办提醒的时候&#xff0c;看到了“业务关停通知”的弹窗&#xff0c;大意就是说&#xff0c;腾讯待办将于2023年12月20日…

【开源项目】snakeflow流程引擎研究

项目地址 https://gitee.com/yuqs/snakerflow https://toscode.mulanos.cn/zc-libre/snakerflow-spring-boot-stater &#xff08;推荐&#xff09; https://github.com/snakerflow-starter/snakerflow-spring-boot-starter 常用API 部署流程 processId engine.process().de…

如何实现公网远程访问本地OpenGauss数据库【内网穿透】

文章目录 前言1. Linux 安装 openGauss2. Linux 安装cpolar3. 创建openGauss主节点端口号公网地址4. 远程连接openGauss5. 固定连接TCP公网地址6. 固定地址连接测试 前言 openGauss是一款开源关系型数据库管理系统&#xff0c;采用木兰宽松许可证v2发行。openGauss内核深度融合…

十七、W5100S/W5500+RP2040树莓派Pico<HTTP Server网页显示>

文章目录 1 前言2 简介2 .1 什么是HTTP&#xff1f;2.2 HTTP的优点2.3 HTTP工作原理2.4 HTTP应用场景 3 WIZnet以太网芯片4 HTTP网络设置示例概述以及使用4.1 流程图4.2 准备工作核心4.3 连接方式4.4 主要代码概述4.5 结果演示 5 注意事项6 相关链接 1 前言 HTTP是互联网上应用…

MapInfo Pro “偏移”命令

偏移对象的用途是什么&#xff1f; 将一个或多个地图对象移动特定距离和/或方向&#xff0c;并将其放置在可编辑层中。对象可以来自任何层。您可以在选择操作后聚合数据。 ​ “偏移对象”何时处于活动状态&#xff1f; 当“贴图”窗口为活动窗口时&#xff0c;该窗口具有可编…

【FastCAE源码阅读8】调用gmsh生成网格

FastCAE使用gmsh进行网格划分&#xff0c;划分的时候直接启动一个新的gmsh进程&#xff0c;个人猜测这么设计是为了规避gmsh的GPL协议风险。 进行网格划分时&#xff0c;其大体运行如下图&#xff1a; 一、Python到gmshModule模块 GUI操作到Python这步不再分析&#xff0c;比…

基于《环境影响评价技术导则大气环境(HJ 2.2-2018)》的AERMOD模型配置方法

数值模式模拟是分析大气污染物时空分布和成分贡献的重要工具&#xff0c;利用模拟结果可以分析大气污染的来源、成因、污染程度、持续时间、主要成分、相对贡献等问题&#xff0c;有助于分析并合理控制污染源排放&#xff0c;为产业调整提供参考。当前&#xff0c;针对不同理论…

深度学习 python opencv 实现人脸年龄性别识别 计算机竞赛

文章目录 0 前言1 项目课题介绍2 关键技术2.1 卷积神经网络2.2 卷积层2.3 池化层2.4 激活函数&#xff1a;2.5 全连接层 3 使用tensorflow中keras模块实现卷积神经网络4 Keras介绍4.1 Keras深度学习模型4.2 Keras中重要的预定义对象4.3 Keras的网络层构造 5 数据集处理训练5.1 …

C++面向对象编程(4)——浅谈C++内存模型

目录 一. 说明 二. GDB实验 2.1 实验1&#xff1a;栈 2.2 实验2&#xff1a;堆 一. 说明 不同的操作系统对程序内存的管理和划分会有所不同。如上图所示的C内存区域划分主要是针对一般的情况&#xff0c;说明如下&#xff1a; 1. Stack&#xff1a;栈。由编译器管理分配和回…

CKA认证模块②-K8S企业运维和落地实战-2

CKA认证模块②-K8S企业运维和落地实战-2 K8S常见的存储方案及具体应用场景分析 k8s存储-empty emptyDir类型的Volume是在Pod分配到Node上时被创建&#xff0c;Kubernetes会在Node上自动分配一个目录&#xff0c;因此无需指定宿主机Node上对应的目录文件。 这个目录的初始内容…

从0到0.01入门React | 005.精选 React 面试题

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…

快速批量去除文件夹名称中多余重复文字!一键轻松优化文件夹命名!

您是否曾经因为文件夹名称中多余重复文字而烦恼&#xff1f;是否因为文件夹重命名而浪费大量时间&#xff1f;现在&#xff0c;我们为您推荐一款全新的文件夹批量改名工具——快速批量去除文件夹名称中多余重复文字&#xff0c;轻松实现文件夹改名优化&#xff0c;让您的整理效…

企业微信后台通过小程序给员工发送文字信息附带超链接实现(加上A标签:<a href=“网址“> </a>)

如下&#xff0c;在编辑文本消息的时候&#xff0c;添加上HTML的A标签 <a href"www.baidu"> </a>即可实现点击直接跳转

移远EC600U-CN开发板 day04

控件探索-滑杆&#xff08;lv.slider&#xff09; 1. 显示一个简单的滑杆 def slider_event_cb(evt): slider evt.get_target()# 修改label的值label.set_text(str(slider.get_value()))slider lv.slider(scr) #创建滑杆组件 slider.set_width(200) #设置滑杆宽…

上门洗衣洗鞋app小程序

上门洗衣洗鞋app小程序作为专业的帮助用户洗衣服务的软件,许多朋友都使用过。在这里,小编就帮助大家收集一些非常不错的洗衣洗鞋软件。 不知道大家是否还在为洗衣而烦恼,而怕麻烦,现在大家都在用网上的洗衣洗鞋小程序来洗衣服,用户只需要打开手机软件,发起订单,门店即可收到订单…

Flink SQL --命令行的使用(02)

1、窗口函数&#xff1a; 1、创建表&#xff1a; -- 创建kafka 表 CREATE TABLE bid (bidtime TIMESTAMP(3),price DECIMAL(10, 2) ,item STRING,WATERMARK FOR bidtime AS bidtime ) WITH (connector kafka,topic bid, -- 数据的topicproperties.bootstrap.servers m…

产品速递 | 璞华采云端,打造降本增效的企业采购订单协同平台

为应对快速变化的市场环境&#xff0c;企业需要建立起更加敏捷、灵活的采购体系&#xff0c;利用数字化手段提高工作效率、降低潜在风险&#xff0c;将是企业构筑新时代竞争壁垒的关键要素。 而控制采购成本对一个企业的经营业绩至关重要。采购成本下降不仅体现在企业现金流出的…

正交试验DOE

它原本是日本学者为了质量管理而设计的试验。后来被用在算法的参数设计上&#xff0c;可以利用部分的试验确定出最合理的参数组合。 举个例子&#xff0c;比如遗传算法中的种群数pop&#xff0c;交叉概率pr&#xff0c;变异概率pm&#xff0c;以及迭代次数N&#xff0c;每个参…

字符串和内存函数(1)

文章目录 目录1. 前言2. 函数介绍2.1 strlen2.2 strcpy2.3 strcat2.4 strcmp2.5 strncpy2.6 strncat2.7 strncmp2.8 strstr2.9 strtok2.10 strerror2.11 字符分类函数2.12 字符转换函数 目录 求字符串长度函数长度不受限制的字符串函数长度受限制的字符串函数字符串查找函数错…