带头双向循环链表的实现

news2024/11/15 15:52:18

目录

  • 前言
  • 节点声明
  • 链表的初始化
  • 尾插
  • 打印链表
  • 头插
  • 尾删
  • 头删
  • 查找节点
  • 指定位置插入
  • 指定位置删除
  • 链表销毁

前言

之前讲过单链表的实现,在实现的过程中,我们会发现每次删除或者在前面插入节点的时候,都要提前保存上一个节点的地址。这样做十分麻烦,所以就有了单链表的升级,双链表。今天就来实现一个带头双向循环的双链表。

带头: 指有一个哨兵位的头节点,头节点不拿来存放有效值
双向: 代表是个双向链表,一个指针存放前一个节点地址,一个地址存放后一个节点地址。
循环: 最后一个节点的下一个节点是头节点。
在这里插入图片描述

节点声明

刚刚提到过,双向链表是有2个指针的,一个指针指向前一个节点,一个指针指向后一个节点,还有一个来存放数据。

//存放的类型
typedef int ListValType;

//节点结构体声明
typedef struct ListNode
{
	ListValType val;
	struct ListNode* prve;
	struct ListNode* next;
}LSNode;

链表的初始化

因为我们是带哨兵头的链表,所以链表需要初始化。
初始化我们只需要开辟一个头节点,但这个节点我们不存放有效值。
而因为是循环链表,所以最后一个节点要指向第一个节点,只有一个节点时,自己指向自己。
在这里插入图片描述
初始化代码

//链表初始化
LSNode* ListInto()
{
	//创建一个节点
	LSNode* newNode = (LSNode*)malloc(sizeof(LSNode));
	//指向自己
	newNode->next = newNode;
	newNode->prve = newNode;
	
	return newNode;
}

在这里插入图片描述
调试后发现它的next和prve指针都指向自己,说明初始化完成了。

尾插

那么我们就可以写个尾部插入来玩玩,我们都知道头节点的前一个地址是指向最后一个地址的。所以就可以直接找到尾节点进行插入。
在这里插入图片描述


//创建节点
LSNode* CreateListNode(ListValType x)
{
	LSNode* newNode = (LSNode*)malloc(sizeof(LSNode));
	if (newNode == NULL)
	{
		//空间开辟失败,不玩了
		exit(-1);
	}
	newNode->val = x;
	return newNode;
}

//尾插
void ListPushBack(LSNode* phead, ListValType x)
{
	//断言,phead不能为空
	assert(phead);

	//创建一个新节点
	LSNode* newNode = CreateListNodeC(x);
	//记录头节点的前一个节点,也就是尾节点
	LSNode* tail = phead->prve;
	//尾节点指向 新节点
	tail->next = newNode;
	//新节点前节点指向尾节点
	newNode->prve = tail;
	//后节点指向头节点
	newNode->next = phead;
	//头节点前节点指向新节点
	phead->prve = newNode;

}

然后调试发现链表已经连起来了
在这里插入图片描述

打印链表

为了方便测试,我们对链表进行打印一下,但这次打印和之前不同,因为之前是打印到最后一个节点为NULL停止,但循环链表不存在NULL节点,所以当我们再次走到头节点的时候停止打印。

//打印链表
void ListPrint(LSNode* phead)
{
	assert(phead);
	//头节点存放的是无效值,所以从头节点下一个位置开始打印
	LSNode* cru = phead->next;
	//cru == phead时说明链表走了一圈了
	while (cru != phead)
	{
		printf("%d->", cru->val);
		cru = cru->next;
	}
	printf(".....\n");
}

在这里插入图片描述
因为是循环链表,所以不可能打印完,我们打印一圈就可以了。

头插

头插也很简单,因为头节点存放的不是有效值,我们只需要记录头节点的下一个节点,然后让它的 prve节点指向新节点,让新节点next节点指向它,头节点的next指向新节点,新节点的prve节点指向头节点。
语言表达有点绕,看图。
在这里插入图片描述
代码

//头插
void ListPushFront(LSNode* phead, ListValType x)
{
	//断言,phead不能为空
	assert(phead);

	//创建一个新节点
	LSNode* newNode = CreateListNode(x);
	//保存头节点的下一个节点
	LSNode* Next = phead->next;
	//next前节点指向新节点
	Next->prve = newNode;
	//新节点next指向Next
	newNode->next = Next;
	//头节点next指向新节点
	phead->next = newNode;
	//新节点前节点指向头节点
	newNode->prve = phead;
}

在这里插入图片描述

尾删

尾删和尾插差不多,不过需要最后一个节点的前一个节点,然后让它指向头节点,随后释放最后一个节点。
在这里插入图片描述
但是我们需要注意一个问题,当尾节点等于头节点时,那么说明链表没有节点了,哨兵头节点是不能删除的,所以这时我们需要判断或者断言一下都行。

//尾删
void ListPopBack(LSNode* phead)
{
	//phead不能为空
	assert(phead);
	//尾节点不能头节点一样
	assert(phead != phead->prve);
	
	//尾节点
	LSNode* tail = phead->prve;
	//尾节点的前一个节点
	LSNode* prvetail = tail->prve;
	//释放尾节点
	free(tail);
	//prvetail 连接 头节点
	prvetail->next = phead;
	phead->prve = prvetail;

}

在这里插入图片描述

然后我们发现后面的3和2都被删掉了

头删

头删我们只需要记录头节点的下一个节点的下一个节点,因为等等头节点要和这个节点连接,然后释放掉头节点的下一个节点即可。
在这里插入图片描述

当然,头删和尾删一样,当链表只剩下头节点时,那就不能再删除了。

//头删
void ListPopFront(LSNode* phead)
{
	assert(phead);
	assert(phead != phead->prve);

	//头节点的下一个节点
	LSNode* headNext = phead->next;
	//下下个节点
	LSNode* nextnext = headNext->next;
	//连接头节点和 nextnext
	nextnext->prve = phead;
	phead->next = nextnext;

	//释放headNext
	free(headNext);
	headNext = NULL;
}

代码执行结果
在这里插入图片描述

查找节点

这个就很简单了,思路和打印差不多,遍历一遍找,没找到返回空指针。

//查找
LSNode* ListFindNode(LSNode* phead, ListValType x)
{
	assert(phead);
	//头节点存放的是无效值,所以从头节点下一个位置开始查找
	LSNode* cru = phead->next;
	//cru == phead时说明链表走了一圈了
	while (cru != phead)
	{
		if (cru->val == x)
			return cru;

		cru = cru->next;
	}
	return NULL;
}

在这里插入图片描述

指定位置插入

指定位置插入和头插尾插没有太大区别,如果前插,保存pos位置的前一个节点,如果后插,保存后一个节点,然后连接。
这里我们演示前插。在这里插入图片描述

//指定插入
void ListInsert(LSNode* phead, LSNode* pos, ListValType x)
{
	//pos和phead不能为空
	assert(pos && phead);
	
	//创建节点
	LSNode* newNode = CreateListNode(x);

	//保存pos的前一个节点
	LSNode* posprve = pos->prve;
	//然后连接起来
	posprve->next = newNode;
	newNode->prve = posprve;
	newNode->next = pos;
	pos->prve = newNode;
	
}

在这里插入图片描述
我们发现这个函数也可以完成头插和尾插,所以说前面的头插和尾插可以直接复用这个函数。
头插更新

//头插
void ListPushFront(LSNode* phead, ListValType x)
{
	/*
	//断言,phead不能为空
	assert(phead);

	//创建一个新节点
	LSNode* newNode = CreateListNode(x);
	//保存头节点的下一个节点
	LSNode* Next = phead->next;
	//next前节点指向新节点
	Next->prve = newNode;
	//新节点next指向Next
	newNode->next = Next;
	//头节点next指向新节点
	phead->next = newNode;
	//新节点前节点指向头节点
	newNode->prve = phead;
	*/
	ListInsert(phead,phead->next,x);
}

尾插更新

//尾插
void ListPushBack(LSNode* phead, ListValType x)
{
	/*
	//断言,phead不能为空
	assert(phead);

	//创建一个新节点
	LSNode* newNode = CreateListNode(x);
	//记录头节点的前一个节点,也就是尾节点
	LSNode* tail = phead->prve;
	//尾节点指向 新节点
	tail->next = newNode;
	//新节点前节点指向尾节点
	newNode->prve = tail;
	//后节点指向头节点
	newNode->next = phead;
	//头节点前节点指向新节点
	phead->prve = newNode;
	*/
	ListInsert(phead, phead->prve, x);

}

指定位置删除

前删的话我们首先保存pos的前一个节点和后一个节点,然后两个节点连接,释放pos即可,也可以先释放,没有顺序要求。
在这里插入图片描述

//指定删除
void ListEase(LSNode* phead, LSNode* pos)
{
	//pos和phead不能为空
	assert(pos && phead);

	//保存pos前后节点
	LSNode* posprve = pos->prve;
	LSNode* posnext = pos->next;

	//前后节点连接
	posprve->next = posnext;
	posnext->prve = posprve;

	//释放pos空间
	free(pos);
	pos = NULL;
}

在这里插入图片描述
我们发现它同样可以完成 头删和尾删,所以头删和尾删我们可以直接复用这个函数。
头删更新

//头删
void ListPopFront(LSNode* phead)
{
	/*
	assert(phead);
	assert(phead != phead->prve);

	//头节点的下一个节点
	LSNode* headNext = phead->next;
	//下下个节点
	LSNode* nextnext = headNext->next;
	//连接头节点和 nextnext
	nextnext->prve = phead;
	phead->next = nextnext;

	//释放headNext
	free(headNext);
	headNext = NULL;
	*/
	ListEase(phead, phead->next);
}

尾删更新

void ListPopBack(LSNode* phead)
{
	/*
	//phead不能为空
	assert(phead);
	//尾节点不能头节点一样
	assert(phead != phead->prve);
	
	//尾节点
	LSNode* tail = phead->prve;
	//尾节点的前一个节点
	LSNode* prvetail = tail->prve;
	//释放尾节点
	free(tail);
	//prvetail 连接 头节点
	prvetail->next = phead;
	phead->prve = prvetail;
	*/

	ListEase(phead, phead->prve);
}

链表销毁

销毁是连头也一起销毁的,所以先保存后一个或前一个地址,然后释放当前地址,然后迭代一个个销毁,直到最后遇到头节点后销毁头节点并结停止迭代在这里插入图片描述

//销毁链表,因为会改变原来的phead节点,所以需要传二级指针
void ListDestroy(LSNode** pphead)
{
	assert(*pphead);
	
	LSNode* cru = (*pphead)->next;
	while (1)
	{
		//保存后一个节点
		LSNode* next = cru->next;
		//释放cru
		free(cru);
		//迭代
		cru = next;
		//如果cru 和头节点相等,说明头节点后面的都删完了
		if (cru == *pphead)
		{
			//释放头节点空间
			free(*pphead);
			//指针置空
			*pphead = NULL;
			//跳出循环
			break;	
		}
	}
}

在这里插入图片描述

我们在调试看看是否真的销毁成功了。
在这里插入图片描述
这样我们的带头双向循环链表已经简单实现完了。代码已上传至git,点此获取

这里还有单链表的实现,有兴趣的大佬也可以看看

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

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

相关文章

大一新生HTML期末作业个人介绍博客 使用html+css+javascript+jquery技术制作网页,含有动画,hover效果,含有表格布局

🎉精彩专栏推荐👇🏻👇🏻👇🏻 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 💂 作者主页: 【主页——🚀获取更多优质源码】 🎓 web前端期末大作业…

RV1126/RV1109 IPC板 + RK3568+鸿蒙AI视频解决方案

近年来,云终端产品在办公、教育、工控等行业被广泛应用,其具有实用性强、运维量小、数据存储更安全等特点,深受市场青睐。而面对复杂光照环境、人流与车流、多变人体动作等复杂场景,成像质量和画面效果以及细节呈现能力&#xff0…

Ansible自动化运维工具之playbook剧本编写(上)

内容预知 1.playbook的相关知识 1.1 playbook 的简介 1.2 playbook的 各部分组成 2. 基础的playbook剧本编写实例 实例1:playbook编写 apache的yum安装部署剧本 实例2:playbook编写nginx 的yum安装并且能修改其监听端口的剧本 3. playbook的定义、引…

网站如何快速变成灰色?,几行代码就搞定了!

当大家看到全站的内容都变成了灰色,包括按钮、图片等等。这时候我们可能会好奇这是怎么做到的呢?有人会以为所有的内容都统一换了一个 CSS 样式,图片也全换成灰色的了,按钮等样式也统一换成了灰色样式。但你想想这个成本也太高了&…

ThreadLocal笔记

并发的场景中,如果有多个线程同时修改公共变量,可能会出现线程安全问题,即该变量最终结果可能出现异常。 如果使用锁来保证资源隔离,会存在大量锁等待,会让响应时间延长很多。 ThreadLocal的核心思想是:共享…

云服务器centos8搭建网站 apache+php+mysql

由于对数据库容量要求比较大,年费用300左右的普通虚拟主机只能提供500M-1G的数据库,不能满足要求,故寻找到同样费用的云服务器单核、1G内存、系统盘50G,缺点是只提供基本系统centos,其他要自己搭建,经过一周…

05_openstack之Neutron网络管理

目录 一、环境准备 二、通过Horizon设置外部网络 1、创建外网网络 2、创建内网网络 3、创建路由 一、环境准备 部署openstack私有云环境:02_openstack私有云部署_桂安俊kylinOS的博客-CSDN博客 创建项目和用户:03_openstack之项目及用户管理_桂安…

fastTEXT论文解读并附实例代码

上一篇博文是入门使用级别,但对于面试来说则不够,毕竟领导一问三不知必定over,其基本原理还是要搞清楚,因而有此博文。paper在此 0,绪论 考虑紧致特征以减少存储空间,提出在PQ( product quant…

三、CANdelaStudio入门-视图类型(View type)

本专栏将由浅入深的展开诊断实际开发与测试的数据库编辑,包含大量实际开发过程中的步骤、使用技巧与少量对Autosar标准的解读。希望能对大家有所帮助,与大家共同成长,早日成为一名车载诊断、通信全栈工程师。 本文介绍CANdelaStudio的各个视图类型:Standard View、Expert V…

Jmeter的使用教程(安装)

前言:之前在工作中未接触过Jmeter,只是知道这个Java语言开发的工具主要是测试接口的,还能做一些性能和压力的测试、并发什么的。目前市面上的有postman、apifox,request、swagger。 一、下载: 1、可以直接到官网下载&a…

(Cascade extended state observer)级联ADRC的simulink仿真和程序---送给中国研究学者的精华版

在这里先声明一下,级联CESO由美国学者Rafal Madonski的论文 《Cascade extended state observer for active disturbance rejection control applications under measurement noise》提出,本人只是将他给的模型给中国学者研究, 模型适用于各…

做了几年“斜杠青年”,我在ZStack立志做国产云计算的研发

在浅黑科技《ZStack:这群做云的人有点“轴”》一文中,作者史中提到,这是一篇国产云计算佼佼者ZStack的创业史,文中记录了因为热爱而聚集起来的最早一批ZStacker,他们生活没有退路,但热爱未有止息。 实际上…

概率论与数理统计_第1章_几何概型

1 定义 若一个试验具有下列两个特征: (1)试验的所有可能结果是无限多个, 且全体结果可以用一个有度量的几何区域 Ω 来表示; (2)每个可能结果 都相同概率可能发生, 则该试验称为几何…

Pytorch:Torch数据类型学习整理与记录

文章目录前言一、Tensor数据类型简介Tensor数据类型是什么?Tensor数据类型有哪些指定调用的API生成相关数据类型dtype属性指定Tensor内置的简单数据类型二、Tensor数据类型的基本使用Tensor初始化基于list列表和nparrayTensor相关API基于指定Tensor类型进行初始化基于Randn生成…

postgres源码解析40 表创建执行全流程梳理--4

本文讲解非系统表的创建逻辑&#xff08;[<fontcolor0000dd>普通表和索引表]&#xff09;&#xff0c;其入口函数为heap_create&#xff0c;内部公共接口函数为RelationBuildLocalRelation和RelationCreateStorage相关知识回顾见&#xff1a; postgres源码解析38 表创建执…

Mac M1使用UTM安装centos7 x86_64虚拟机

一、环境说明 1. 宿主机环境 macbook m1 pro 16G 2. UTM版本 UTM是基于QEMU的系统模拟器和虚拟机主机&#xff0c;适用于iOS和macOS。 UTM is a full featured system emulator and virtual machine host for iOS and macOS. It is based off of QEMU. 最新版下载地址&…

带你玩转序列模型之NLP与词嵌入(二)

目录 一.Word2Vec 二.负采样 三.GloVe词向量 四.情绪分类 五.词嵌入除偏 一.Word2Vec 在上个视频中你已经见到了如何学习一个神经语言模型来得到更好的词嵌入&#xff0c;在本视频中你会见到 Word2Vec算法&#xff0c;这是一种简单而且计算时更加高效的方式来学习这种类…

用于 Python 降维的主成分分析

减少预测模型的输入变量数称为降维。 较少的输入变量可以产生更简单的预测模型&#xff0c;该模型在对新数据进行预测时可能具有更好的性能。 也许机器学习中最流行的降维技术是主成分分析&#xff0c;简称PCA。这是一种来自线性代数领域的技术&#xff0c;可用作数据准备技术…

耗时大半个月收整全套「Java架构进阶pdf」

花了我大半个月时间收整了全套的「Java架构进阶pdf」&#xff0c;这一波下来&#xff0c;刷完你就会知道&#xff0c;真真香啊&#xff0c;我的心血果然&#xff0c;没白费&#xff01; 请注意&#xff1a;关于全套的「Java架构进阶pdf」&#xff0c;我会从面试-筑基-框架-分布…

【Android App】实战项目之仿微信的视频通话(附源码和演示 超详细必看)

需要源码请点赞关注收藏后评论区留言私信~~~ 虽然手机出现许多年了&#xff0c;它具备的功能也越来越丰富&#xff0c;但是最基本的通话功能几乎没有变化。从前使用固定电话的时候&#xff0c;通话就是听声音&#xff1b;如今使用最新的智能手机&#xff0c;通话仍旧是听声音。…