【数据结构】双向带头循环链表(c语言)(附源码)

news2024/11/13 8:57:49

🌟🌟作者主页:ephemerals__

🌟🌟所属专栏:数据结构

目录

前言

1.双向带头循环链表的概念和结构定义

2.双向带头循环链表的实现

2.1 方法声明

2.2 方法实现

2.2.1 创建新节点

2.2.2 初始化

2.2.3 打印

2.2.4 判断链表是否为空

2.2.5 尾插

2.2.6 头插

2.2.7 尾删

2.2.8 头删

2.2.9 查找

2.2.10 指定位置之前插入

2.2.11 指定位置之后插入

2.2.12 删除指定位置节点

2.2.13 销毁链表

3.程序全部代码

总结


前言

        我们常用的链表有两种:

单向无头不循环链表:也就是我们所说的单链表,它的结构简单,一般是不会用于单独存放数据的。它常被用于实现哈希桶、图的邻接表等。

双向带头循环链表:通常称为双向链表,它的结构较为复杂,实际使用中用于单独存放数据。虽然它的结构比较复杂,但是它的方法执行效率要高于单链表

接下来,就让我们学习并尝试实现双向带头循环链表。

1.双向带头循环链表的概念和结构定义

双向带头循环链表(双向链表)有三个关键点

1.双向:不同于单链表,双向链表的节点的指针域附带有两个指针,分别指向其前驱节点和后继节点,这便于我们更灵活地访问链表元素。

2.带头:这里的“头”指的是“哨兵位”,也就是说在创建链表时先创建一个哨兵位的节点位于头部,此节点不存放任何有效数据,只是起到“放哨”的作用

3.循环:也就是说链表尾部不指向空指针,而是指向头部的节点,形成一个“环”状结构

而对于单链表,由于不具备这三个特性,所以在运行效率上要低于双向链表。那么我们来看看它的结构定义:

typedef int LTDataType;

//双向链表的节点定义
typedef struct ListNode
{
	LTDataType data;//数据域
	struct ListNode* next;//指向前驱节点的指针
	struct ListNode* prev;//指向后继节点的指针
}LTNode;

2.双向带头循环链表的实现

        接下来,我们尝试实现它的一些功能。首先是方法的声明:

2.1 方法声明

//创建新节点
LTNode* LTBuyNode(LTDataType n);

//初始化,创建哨兵
void LTInit(LTNode** pphead);

//打印链表
void LTPrint(LTNode* phead);

//判断链表是否为空
bool LTEmpty(LTNode* phead);

//尾插
void LTPushBack(LTNode* phead,LTDataType n);

//头插
void LTPushFront(LTNode* phead, LTDataType n);

//尾删
void LTPopBack(LTNode* phead);

//头删
void LTPopFront(LTNode* phead);

//查找
LTNode* LTFind(LTNode* phead, LTDataType n);

//指定位置之前插入
void LTInsert(LTNode* pos, LTDataType n);

//指定位置之后插入
void LTInsertAfter(LTNode* pos, LTDataType n);

//删除指定节点
void LTErase(LTNode* pos);

//销毁链表
void LTDestroy(LTNode** pphead);

2.2 方法实现

2.2.1 创建新节点

        创建新节点的方式于单链表相似,但由于循环的特性,要暂时将其next指针和prev指针指向自己:

//创建新节点
LTNode* LTBuyNode(LTDataType n)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));//动态申请内存
	if (newnode == NULL)//申请失败,退出程序
	{
		perror("malloc");
		exit(1);
	}
	newnode->data = n;
	newnode->next = newnode->prev = newnode;//让两个指针都指向自己
	return newnode;//返回该节点
}

2.2.2 初始化

        初始化时,我们需要创建一个哨兵节点,并且让头指针指向它。由于修改了头指针的值,所以要传入二级指针。

//初始化,创建哨兵
void LTInit(LTNode** pphead)
{
	assert(pphead);//避免传入空指针
	*pphead = LTBuyNode(-1);//创建哨兵节点,传无效数据
}

2.2.3 打印

        对于打印操作,我们从哨兵的next节点开始,按顺序向后遍历打印即可。这里需要注意一下循环的结束条件

//打印链表
void LTPrint(LTNode* phead)
{
	LTNode* cur = phead->next;//从头节点的下一个节点开始遍历
	while (cur != phead)//由于链表为循环链表,一轮遍历之后还会走到头节点的位置,所以就以头节点为结束标志
	{
		printf("%d ", cur->data);//打印数据
		cur = cur->next;//向后遍历
	}
	printf("\n");
}

2.2.4 判断链表是否为空

        将判空操作单独封装为一个函数,便于其他方法使用。

//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
	assert(phead);//防止传空指针
	return phead == phead->next;//后继节点为头节点本身,则说明链表为空,返回true,否则返回false
}

2.2.5 尾插

        与单链表不同,尾插的操作不需要遍历找到链表末尾,头节点的prev指针就是链表的尾节点

代码如下:

//尾插
void LTPushBack(LTNode* phead, LTDataType n)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(n);//创建新节点
	newnode->next = phead;//新节点的next指向头节点
	newnode->prev = phead->prev;//新节点的prev指向当前的尾节点
	phead->prev->next = newnode;//当前尾节点的next指向新节点
	phead->prev = newnode;//头节点的prev指向新节点
}

2.2.6 头插

        头插的操作过程与尾插十分相似,注意要在头节点的下个节点处插入。

代码如下:

//头插
void LTPushFront(LTNode* phead, LTDataType n)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(n);
	newnode->next = phead->next;//新节点的next指向当前的第一个节点
	newnode->prev = phead;//新节点的prev指向头节点
	phead->next->prev = newnode;//当前第一个节点的prev指向新节点
	phead->next = newnode;//头节点的next指向新节点
}

2.2.7 尾删

        尾删操作时,注意针对的是头节点的prev节点。

代码如下:

//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead && !LTEmpty(phead));//注意链表不能为空
	LTNode* del = phead->prev;//要删除的节点
	LTNode* prev = del->prev;//要删除节点的前驱节点
	prev->next = phead;//前驱节点的next指向头节点
	phead->prev = prev;//头节点的prev指向前驱节点
	free(del);//释放del的内存
	del = NULL;//及时制空
}

2.2.8 头删

        头删的操作与尾删相似,针对的是头节点的next节点。

代码如下:

//头删
void LTPopFront(LTNode* phead)
{
	assert(phead && !LTEmpty(phead));
	LTNode* del = phead->next;//要删除的节点
	LTNode* next = del->next;//要删除节点的后继节点
	next->prev = phead;//后继节点的prev指向头节点
	phead->next = next;//头节点的next指向后继节点
	free(del);
	del = NULL;
}

2.2.9 查找

        与单链表相同,查找操作也需要遍历链表,匹配成功则返回该节点;找不到则返回空指针。

//查找
LTNode* LTFind(LTNode* phead, LTDataType n)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == n)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

2.2.10 指定位置之前插入

        进行指定位置插入时,注意确定指定位置的前驱节点和后继节点。

//指定位置之前插入
void LTInsert(LTNode* pos, LTDataType n)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(n);
	newnode->next = pos;//新节点的next指向pos
	newnode->prev = pos->prev;//新节点的prev指向pos的前一节点
	pos->prev->next = newnode;//pos的前节点的next指向newnode
	pos->prev = newnode;//pos的prev指向newnode
}

2.2.11 指定位置之后插入

//指定位置之后插入
void LTInsertAfter(LTNode* pos, LTDataType n)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(n);
	newnode->next = pos->next;//新节点的next指向pos的后一节点
	newnode->prev = pos;//新节点的prev指向pos
	pos->next->prev = newnode;//pos的后一节点的prev指向newnode
	pos->next = newnode;//pos的next指向newnode
}

2.2.12 删除指定位置节点

//删除指定节点
void LTErase(LTNode* pos)
{
	assert(pos);
	pos->next->prev = pos->prev;//pos的后继节点的prev指向pos的前驱节点
	pos->prev->next = pos->next;//pos的前驱节点的next指向pos的后继节点
	free(pos);
	pos = NULL;
}

2.2.13 销毁链表

        销毁链表时,我们需要遍历链表按照顺序删除全部节点,最后记得要删除头节点

//销毁链表
void LTDestroy(LTNode** pphead)
{
	assert(pphead);
	if (*pphead == NULL)//链表已经被销毁的情况
	{
		return;
	}
	LTNode* cur = (*pphead)->next;//从第一个节点开始遍历
	while (cur != *pphead)
	{
		LTNode* next = cur->next;//记录后继节点
		free(cur);//释放内存
		cur = next;//使cur指向记录的后继节点
	}
	cur = NULL;
	free(*pphead);//删除头节点
	*pphead = NULL;
}

3.程序全部代码

        程序全部代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

typedef int LTDataType;

//双向链表的节点定义
typedef struct ListNode
{
	LTDataType data;//数据域
	struct ListNode* next;//指向前驱节点的指针
	struct ListNode* prev;//指向后继节点的指针
}LTNode;

//创建新节点
LTNode* LTBuyNode(LTDataType n);

//初始化,创建哨兵
void LTInit(LTNode** pphead);

//打印链表
void LTPrint(LTNode* phead);

//判断链表是否为空
bool LTEmpty(LTNode* phead);

//尾插
void LTPushBack(LTNode* phead,LTDataType n);

//头插
void LTPushFront(LTNode* phead, LTDataType n);

//尾删
void LTPopBack(LTNode* phead);

//头删
void LTPopFront(LTNode* phead);

//查找
LTNode* LTFind(LTNode* phead, LTDataType n);

//指定位置之前插入
void LTInsert(LTNode* pos, LTDataType n);

//指定位置之后插入
void LTInsertAfter(LTNode* pos, LTDataType n);

//删除指定节点
void LTErase(LTNode* pos);

//销毁链表
void LTDestroy(LTNode** pphead);

//创建新节点
LTNode* LTBuyNode(LTDataType n)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));//动态申请内存
	if (newnode == NULL)//申请失败,退出程序
	{
		perror("malloc");
		exit(1);
	}
	newnode->data = n;
	newnode->next = newnode->prev = newnode;//让两个指针都指向自己
	return newnode;//返回该节点
}

//初始化,创建哨兵
void LTInit(LTNode** pphead)
{
	assert(pphead);//避免传入空指针
	*pphead = LTBuyNode(-1);//创建哨兵节点,传无效数据
}

//打印链表
void LTPrint(LTNode* phead)
{
	LTNode* cur = phead->next;//从头节点的下一个节点开始遍历
	while (cur != phead)//由于链表为循环链表,一轮遍历之后还会走到头节点的位置,所以就以头节点为结束标志
	{
		printf("%d ", cur->data);//打印数据
		cur = cur->next;//向后遍历
	}
	printf("\n");
}

//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
	assert(phead);//防止传空指针
	return phead == phead->next;//后继节点为头节点本身,则说明链表为空,返回true,否则返回false
}

//尾插
void LTPushBack(LTNode* phead, LTDataType n)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(n);//创建新节点
	newnode->next = phead;//新节点的next指向头节点
	newnode->prev = phead->prev;//新节点的prev指向当前的尾节点
	phead->prev->next = newnode;//当前尾节点的next指向新节点
	phead->prev = newnode;//头节点的prev指向新节点
}

//头插
void LTPushFront(LTNode* phead, LTDataType n)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(n);
	newnode->next = phead->next;//新节点的next指向当前的第一个节点
	newnode->prev = phead;//新节点的prev指向头节点
	phead->next->prev = newnode;//当前第一个节点的prev指向新节点
	phead->next = newnode;//头节点的next指向新节点
}

//尾删
void LTPopBack(LTNode* phead)
{
	assert(phead && !LTEmpty(phead));//注意链表不能为空
	LTNode* del = phead->prev;//要删除的节点
	LTNode* prev = del->prev;//要删除节点的前驱节点
	prev->next = phead;//前驱节点的next指向头节点
	phead->prev = prev;//头节点的prev指向前驱节点
	free(del);//释放del的内存
	del = NULL;//及时制空
}

//头删
void LTPopFront(LTNode* phead)
{
	assert(phead && !LTEmpty(phead));
	LTNode* del = phead->next;//要删除的节点
	LTNode* next = del->next;//要删除节点的后继节点
	next->prev = phead;//后继节点的prev指向头节点
	phead->next = next;//头节点的next指向后继节点
	free(del);
	del = NULL;
}

//查找
LTNode* LTFind(LTNode* phead, LTDataType n)
{
	assert(phead);
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == n)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

//指定位置之前插入
void LTInsert(LTNode* pos, LTDataType n)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(n);
	newnode->next = pos;//新节点的next指向pos
	newnode->prev = pos->prev;//新节点的prev指向pos的前一节点
	pos->prev->next = newnode;//pos的前节点的next指向newnode
	pos->prev = newnode;//pos的prev指向newnode
}

//指定位置之后插入
void LTInsertAfter(LTNode* pos, LTDataType n)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(n);
	newnode->next = pos->next;//新节点的next指向pos的后一节点
	newnode->prev = pos;//新节点的prev指向pos
	pos->next->prev = newnode;//pos的后一节点的prev指向newnode
	pos->next = newnode;//pos的next指向newnode
}

//删除指定节点
void LTErase(LTNode* pos)
{
	assert(pos);
	pos->next->prev = pos->prev;//pos的后继节点的prev指向pos的前驱节点
	pos->prev->next = pos->next;//pos的前驱节点的next指向pos的后继节点
	free(pos);
	pos = NULL;
}

//销毁链表
void LTDestroy(LTNode** pphead)
{
	assert(pphead);
	if (*pphead == NULL)//链表已经被销毁的情况
	{
		return;
	}
	LTNode* cur = (*pphead)->next;//从第一个节点开始遍历
	while (cur != *pphead)
	{
		LTNode* next = cur->next;//记录后继节点
		free(cur);//释放内存
		cur = next;//使cur指向记录的后继节点
	}
	cur = NULL;
	free(*pphead);//删除头节点
	*pphead = NULL;
}

总结

        今天我们学习了双向带头循环链表的概念以及功能实现。可以发现,与单链表不同,它的头插、尾插、头删、尾删等操作的时间复杂度都是O(1),大大提升了运行效率。之后博主回合大家分享栈和队列的内容。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤

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

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

相关文章

C# 写入SQLServer数据库报错SqlException: 不能将值 NULL 插入列 ‘ID‘

private int id; [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)]//id自增 public int ID { get > id; set > id value; } 将ID属性下的标识规范由否改成是

WebLogic 9.x 10.x中间件监控指标解读

监控易是一款功能强大的IT系统监控软件&#xff0c;能够实时监控包括WebLogic中间件在内的各类应用和业务运行状态。对于WebLogic 9.x和10.x版本的监控&#xff0c;监控易提供了一系列详尽的指标&#xff0c;确保用户能够全面了解和掌握WebLogic集群和应用的性能状况。 在WebLo…

2024年国际高校数学建模竞赛问题B:空间迁移计划和战略完整思路 模型 代码 结果分享(仅供学习)

2024年国际高校数学建模竞赛问题B&#xff1a;空间迁移计划和战略&#xff08;2024 International Mathematics Molding Contest for Higher Education (IMMCHE)Problem B: Space Migration Program and Strategy&#xff09; 我们的未来有两种可能性:第一&#xff0c;我们将留…

目标检测自顶向下入门

最近在学习Yolo和OpenCV这些计算机视觉的相关领域&#xff0c;把深度学习啃了个大概&#xff0c;准备着手学习一下Yolov5&#xff0c;趁着这个机会入门一下目标检测这个领域&#xff0c;也算是自顶向下地学习一遍吧。 目标检测 什么是目标检测 物体识别&#xff08;Object de…

JavaScript(16)——定时器-间歇函数

开启定时器 setInterval(函数,间隔时间) 作用&#xff1a;每隔一段时间调用这个函数&#xff0c;时间单位是毫秒 例如&#xff1a;每一秒打印一个hello setInterval(function () { document.write(hello ) }, 1000) 注&#xff1a;如果是具名函数的话不能加小括号&#xf…

算法板子:使用数组模拟双链表——初始化链表、插入结点、删除结点

插入操作的指针修改顺序&#xff1a; 代码&#xff1a; #include <iostream> using namespace std;const int N 1e5 10;// e[i]代表i结点的值; l[i]代表i结点左边结点的下标; r[i]代表i结点右边结点的下标; idx代表当前可用结点的下标 int e[N], l[N], r[N], idx;// 初…

一刷代码随想录(回溯4)

递增子序列 题意&#xff1a; 给定一个整型数组, 你的任务是找到所有该数组的递增子序列&#xff0c;递增子序列的长度至少是2。 示例: 输入: [4, 6, 7, 7]输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]] 说明: 给定数组的长度不会…

vue3里将table表格中的数据导出为excel

想要实现前端对表格中的数据进行导出&#xff0c;这里推荐使用xlsx这个依赖库实现。 1、安装 pnpm install xlsx 2、使用 import * as XLSX from "xlsx"; 直接在组件里导入XLSX库&#xff0c;然后给表格table通过ref创建响应式数据拿到table实例&#xff0c;将实…

多机构发布智能锁2024半年报:德施曼上半年线上全渠道销额稳居第一

近日&#xff0c;权威机构奥维云网、洛图科技先后发布智能门锁2024半年报&#xff0c;报告均指出上半年中国智能门锁线上渠道持续增长。奥维云网数据显示&#xff0c;2024上半年线上渠道销量同比增长22.7%&#xff0c;成行业增长最快的部分&#xff1b;洛图科技强调&#xff0c…

【React学习打卡第五天】

性能优化相关API、编写类API与zustand 一、useReducer1.基础用法2.分派action时传参 二、useMemo1.基础语法 三、React.memo1.基础语法2.React.memo - props的比较机制 四、useCallback基础语法 五、React.forwardRef六、useInperativeHandle七、类组件编写1.基础结构2.生命周期…

【Linux】:进程间通信及管道

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家带来进程间通信相关知识点&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通 数据结…

“智能体风”吹进体育圈 粉丝手搓上百个智能体为中国健儿应援 太有AI了!粉丝手搓上百个智能体为中国健儿打CALL

智能体的风吹进了体育竞技圈。近日&#xff0c;在百度文心智能体平台&#xff0c;出现了上百个充满“AI”的运动明星粉丝应援智能体&#xff0c;比如支持中国女子乒乓球运动员孙颖莎的“孙颖莎的小迷妹”、支持中国女子跳水队员全红婵的“婵婵的小书包”&#xff0c;应援中国女…

中国医疗AI领头羊讯飞医疗:最新招股书显示前三月收入破亿大关!

讯飞医疗&#xff0c;医疗AI创新企业&#xff0c;收入领先市场。计划港交所上市&#xff0c;用于研发升级、产品扩展及并购。市场潜力巨大&#xff0c;未来发展可期&#xff0c;将成医疗AI璀璨明星。 各位看官&#xff0c;最近科技圈儿又有大新闻啦&#xff01;讯飞医疗科技股份…

时间序列分析方法之 -- 自回归模型(Autoregressive Model, AR)

目录 原理 适用情况 Python 示例代码 结论 原理 自回归模型&#xff08;Autoregressive Model, AR&#xff09;是一种时间序列模型&#xff0c;用于描述一个时间序列的当前值与其过去值之间的关系。自回归模型假设时间序列的当前值是其过去若干值的线性组合&#xff0c;并…

Github 2024-07-26开源项目日报 Top10

根据Github Trendings的统计,今日(2024-07-26统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Java项目2TypeScript项目2C++项目2HTML项目1Python项目1C#项目1Lua项目1JavaScript项目1Vue项目1C项目1免费编程学习平台:freeCodeCamp.org 创…

HANA-sum函数与sum() over(partition by ... order by ... )

sum函数与sum() over(partition by … order by … ) sum()函数就不介绍了。 sum() over(partition by … order by … )其实就是累加的过程具体化。 比如 有1,2,3,4 sum&#xff08;&#xff09;就会得到10 sum() over(partition by … order by … ) 就会得到&#xff1a;1,3…

leetocde662. 二叉树最大宽度,面试必刷题,思路清晰,分点解析,附代码详解带你完全弄懂

leetocde662. 二叉树最大宽度 做此题之前可以先做一下二叉树的层序遍历。具体题目如下&#xff1a; leetcode102二叉树的层序遍历 我也写过题解&#xff0c;可以先看看学习一下&#xff0c;如果会做层序遍历了&#xff0c;那么这题相对来说会简单很多。 具体题目 给你一棵…

数据结构 链式存储 +

int DeleteLinkList(LinkList *list, char *name); int ReviseLinkList(LinkList *list, char *name, DATATYPE data); int DestroyLinkList(LinkList *list); int InsertTailLinkList(LinkList *list, DATATYPE data); ​​​​​​​删除 修改​​​​​​​ 销毁 ​​​​​…

Anaconda、Pytorch安装

Anaconda 打开 Anaconda 官网 https://www.anaconda.com/ 点击右上角的 Free Download 可以选择相应的型号进行下载 如果版本不合适&#xff0c;可以进入 anaconda 的历史版本官网选择适合本机 python 版本的 anaconda 进行下载&#xff1a; https://repo.anaconda.com/arc…

Django-3.3创建模型

创建模型&#xff08;models&#xff09;的时候&#xff0c; 1&#xff1a;我们需要这个模型是哪个文件下面的模型&#xff08;models&#xff09;&#xff0c;我们需要在配置文件中吧应用安装上&#xff08;安装应用&#xff1a;INSTALLED_APPS&#xff09; 2&#xff1a;找对…