详解双向带头循环链表

news2024/11/24 19:33:48

今天给大家分享的数据结构中的链表的双向带头循环链表结构!听到这个链表大家可能心中一颤,其实他就是个纸老虎,看着比较难搞,实际上非常简单易懂,创建该结构链表的大佬可谓是真的牛,因为该结构比起单链表要好的不是一星半点了!下面来看一下双向带头循环链表的样子

 

可以看出该链表的head的前面那个指针指向最后一个结点,最后一个结点的next指针指向head指针,构成了循环!

下面来详细解释一下何谓双向带头循环链表! 

双向带头循环链表的解释!

带头

首先先来解释一下带头的意思,带头用书面上的话就是引用了哨兵位!简单来说,就是单独开辟了一块空间用来存储指向头结点指针的地址!这一份空间和其他结点相同,不同的是该结点的数值域不存储有效数值!有些课本上会将哨兵位的数值域存储该链表的长度,实际上这是不合理的,若数值为整形类型,这是可以的,但是若换成char类型,其远远不能存放结点的个数!double类型同理!因为有了哨兵位的存在,可以使得我们下面有关链表的功能省下不少功夫!

循环

循环的意思是该链表是一个环形的结构!不能像以前的单链表那种方式来遍历整个链表了!

双向

顾名思义,该链表中的每个结点与前后两个结点都有联系,不同于单链表的是,双向链表的指针域存放的是上一个结点的地址和下一个结点的地址!

双向带头链表各个功能块的具体实现!

代码的规范性,方便后续修改!

为了方便以后链表的数据可以进行修改,采用类型重定义的方式来存储数据,以后改变数据类型只需要将类型改变即可!设置如下代码

//将int类型重命名为Datatype,方便以后的修改!
typedef int Datatype;
//将struct ListNode结构体类型重命名为SLnode!
typedef struct ListNode
{
	Datatype data;
	struct ListNode* pre;
	struct ListNode* next;
}SLnode;

结点的创建(Buynode)

//新建链表的结点!
SLnode* Buynode(Datatype x)
{
	SLnode* newnode = (SLnode*)malloc(sizeof(SLnode));
    //当malloc开辟失败时,返回NULL!
	if (newnode == NULL)
	{
		perror("malloc error");
		return NULL;
	}
	else
	{
		newnode->data = x;
		newnode->next = NULL;
		newnode->pre = NULL;
	}
	return newnode;
}

链表的初始化

//链表的初始化!
SLnode* Initnode()
{
    //初始化创建哨兵位方便后面的操作!
    //因为是双向带头循环,所以当没有结点的时候哨兵位的pre和next都指向自己本身!
	SLnode*phead = Buynode(0);
	phead->next = phead;
	phead->pre = phead;
	return phead;
}

 链表的打印(Print)

//链表的打印!
void Print(SLnode* phead)
{
	SLnode* cur = phead->next;
	printf("guard<==>");
	while (cur != phead)
	{

		printf("%d==>", cur->data);
		cur = cur->next;
	}
	printf("guard\n");
}

因为为循环链表,所以不能像以前单链表那样遍历链表,因为最后一个结点的next指针哨兵位!所以当cur为哨兵位phead,结束循环!开始遍历的头结点也是哨兵位的后面第一个结点phead->next!

判断是否为空链表(ifempty)

//判断是否位空链表!
bool ifempty(SLnode* phead)
{
    //哨兵位不能为空!
    assert(pheaad);

	//若为空则返回1!非空返回0!
	return phead==phead->next;
}

 链表的头删功能(pophead)

//链表的头删功能!
void pophead(SLnode* phead)
{
	assert(phead); //当链表为空时,phead也不能为空!
	assert(!(ifempty(phead)));//当为空链表时,程序直接结束并且打印出错误行!
	//first指针指向第一个结点的地址!
    SLnode* first = phead->next;
    //second指针指向第二个结点的地址!
	SLnode* Second = phead->next->next;
	free(first);
	phead->next = Second;
	Second->pre = phead;
}

实现头删功能仅需要将第一个结点删除,然后让哨兵位的next指向第二个结点,第二个结点的pre指向哨兵位!然后将第一个first结点释放即可!

 链表的头增功能(pushhead)

//链表的头增功能!
void pushhead(SLnode* phead,Datatype x)
{
	//创建一个first变量用来记录原来第一个结点的位置!
	SLnode* first = phead->next;
	SLnode* newnode = Buynode(x);
	phead->next = newnode;
	newnode->next = first;
	newnode->pre = phead;
	first->pre = newnode;
}

 实现头增功能仅需要将新建结点的pre指向哨兵位,哨兵位的next指向新建的结点,新建结点的next指向原来第一个结点即可!第一个结点的pre指向新建结点!

链表的尾增操作(pushend)

//链表的尾增操作!
void pushend(SLnode*phead, Datatype x)
{
	SLnode* tail = phead->pre;//tail用来记录原尾结点!
	SLnode* newnode = Buynode(x);//newnode是将要新插得尾结点!
	tail->next = newnode;	
	newnode->pre = tail;
	newnode->next = phead;
	phead->pre = newnode;
}

 因为有了哨兵位的存在所以不需要改变头结点的指针,所以传一级指针就可以改变结构体!

也不用和单链表那样分情况来考虑尾增的情况,只需要找到尾结点然后进行插入操作即可!

链表的尾删操作 (popend)

//链表的尾删操作!
void popend(SLnode* phead)
{
    assert(phead);
	assert(!(ifempty(phead)));
	SLnode* tail = phead->pre;
	SLnode* tailpre = tail->pre;
	free(tail);
	tailpre->next = phead;
	phead->pre = tailpre;
}

 尾删操作仅需要找到尾结点的前一个结点,然后释放尾结点,最后把尾结点前那个结点的指针关系与哨兵位关系修改即可!

链表的查找功能 (Find)

//链表的查找功能!
SLnode* Find(SLnode* phead, Datatype x)
{
	SLnode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

 当找到与x值对应的cur指针时,返回cur指针!否则返回NULL!(既然实现了链表的查找功能,那么在某个位置的删除,添加也可以实现!

 删除pos对应的位置(erase)

//删除pos对应的位置!
void erase(SLnode* phead, SLnode* pos)
{
    assert(pos);
	assert(!(ifempty(phead)));
	SLnode* pre = pos->pre;
	SLnode* next = pos->next;
	free(pos);
	pre->next = next;
	next->pre = pre;
}

 要想删除pos的位置,只需要知道pos前面结点的地址和其后面结点的地址即可,然后修改指针指向,free掉pos位置即可!

在pos之前插入数据(Insert)

//在pos之前插入数据!
void Insert(SLnode* pos,Datatype x)
{
	SLnode* pre = pos->pre;
	SLnode* newnode = Buynode(x);


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

 在pos位置之前插入数据,只需要知道pos指针的地址即可,然后将指针进行如上代码修改即可完成pos之前插入数据!

销毁链表(destroy)

//销毁链表!
void destroy(SLnode* phead)
{
	assert(phead);
	SLnode* cur = phead->next;
	while (cur != phead)
	{
		SLnode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(phead);
}

 销毁链表,即把每个结点释放掉,然后进行置空即可,从哨兵位的下一个结点开始,直到再次走到哨兵位结束!因为传的是一级指针,所以不能将哨兵位不能进行置空,所以在最后使用过destroy函数之后手动置空即可!

 erase和Insert的复用!

 既然讲了erase函数和Insert函数的实现,那么该函数方便了我们不少,我们可以将其复用到头尾插,头尾删的函数之中!

头插复用insert

头插只需要在head的next位置插入即可,头插复用Insert函数代码如下:

void pushhead(SLnode* phead,Datatype x)
{
    assert(phead);
    Insert(phead->next, x);
}

尾插复用insert 

尾插只需要在最后一个结点的next插入即可,最后插入的那个结点的next是指向phead的,所以在phead(哨兵位)之前插入即可,因为在phead之前插入后其next指向phead,符合最后一个结点的特征!链表图如下:

 

代码如下! 

void pushend(SLnode*phead, Datatype x)
{
	SLnode* tail = phead->pre;//tail用来记录原尾结点!
	SLnode* newnode = Buynode(x);//newnode是将要新插得尾结点!
	Insert(phead, x);
}

头,尾删复用erase

头删即删除第一个结点的位置,即head的next,

尾删即删除最后一个结点的位置,即head的pre,

代码如下:

void pophead(SLnode* phead)
{
	assert(phead); //当链表为空时,phead也不能为空!
	assert(!(ifempty(phead)));//当为空链表时,程序直接结束并且打印出错误行!
	erase(phead,phead->next);//头删复用erase!
    erase(phead,phead->pre); //尾删复用erase!
}

 

  erase和Insert复用的好处!

通过二者函数的复用,可以让我们不用在写头尾增删的函数了,因为二者函数包含了头尾增删的功能!通过这两个函数可以看出创建这个双向带头循环链表的大佬是多么的牛啦!它可以更方便的实现单链表的功能!这还可以让我们手撕链表省下大把时间!

 

好了,今日的双向带头循环链表分享结束,若还有疑问等,欢迎评论区讨论!

 

 

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

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

相关文章

Java课题笔记~ HTTP协议(请求和响应)

Servlet最主要的作用就是处理客户端请求&#xff0c;并向客户端做出响应。为此&#xff0c;针对Servlet的每次请求&#xff0c;Web服务器在调用service()方法之前&#xff0c;都会创建两个对象 分别是HttpServletRequest和HttpServletResponse。 其中HttpServletRequest用于封…

8.10论文阅读

文章目录 The multimodal MRI brain tumor segmentation based on AD-Net摘要本文方法损失函数 实验结果 max-vit - unet:多轴注意力医学图像分割摘要本文方法实验结果 The multimodal MRI brain tumor segmentation based on AD-Net 摘要 基于磁共振成像(MRI)的多模态胶质瘤…

SpringBoot笔记:SpringBoot 集成 Dataway 多数据源配置(二)

文章目录 前言核心代码和配置yml 配置注入多数据源常用Spi实现swagger 配置自定义 Udf指定数据源进行查询 前言 之前简单介绍了一下 Dataway 使用&#xff0c;本文继续介绍一下它的多数据源配置和使用。 核心代码和配置 yml 配置 # springboot多环境配置 #端口&#xff0c;…

SpringBoot中间件使用之EventBus、Metric、CommandLineRunner

1、EventBus 使用EventBus 事件总线的方式可以实现消息的发布/订阅功能&#xff0c;EventBus是一个轻量级的消息服务组件&#xff0c;适用于Android和Java。 // 1.注册事件通过 EventBus.getDefault().register(); // 2.发布事件 EventBus.getDefault().post(“事件内容”); …

【HarmonyOS】Java如何引用外部jar包

【关键字】 Java、引用jar包​ 【写在前面】 使用API6和API7开发HarmonyOS应用时&#xff0c;因为应用中只能引用SDK中开放的功能接口&#xff0c;但是部分jdk自带的接口功能在SDK中并未封装&#xff0c;要想在工程中使用jdk开放的接口功能&#xff0c;需要将jdk中的jar包通过…

【华秋干货铺】电源PCB设计汇总

在《PCB设计丨电源设计的重要性》一文中&#xff0c;已经介绍了电源设计的总体要求&#xff0c;以及不同电路的相关布局布线等知识点&#xff0c;那么本篇内容&#xff0c;小编将以RK3588为例&#xff0c;为大家详细介绍其他支线电源的PCB设计。 电源PCB设计 VDD_CPU_BIG0/1 …

uniapp 格式化时间刚刚,几分钟前,几小时前,几天前…

效果如图&#xff1a; 根目录下新建utils文件夹&#xff0c;文件夹下新增js文件&#xff0c;文件内容&#xff1a; export const filters {dateTimeSub(data) {if (data undefined) {return;}// 传进来的data必须是日期格式&#xff0c;不能是时间戳//将字符串转换成时间格式…

63、64、65、66项目实战

erp项目启动&#xff1a; nohup java -jar lemon_erp.jar & 或 java -jar lemon_erp.jar 把node_exporter监控收集器启动&#xff1a; nohup ./node_exporter & 监控机器上&#xff1a; 启动 grafana 和 prometheus 启动grafana&#xff1a; systemctl restart grafana…

机器学习实战2-决策树算法

文章目录 决策树算法核心是要解决两个的关键问题sklearn中的决策树模型sklearn建模步骤分类树Criterionrandom_state && splitter剪枝参数max_depthmin_samples_leaf&&min_samples_splitmax_features&&min_impurity_decrease确认最优剪枝参数目标权重参…

谈谈Spring与字节码生成技术

Java程序员几乎都了解Spring。 它的IoC&#xff08;依赖反转&#xff09;和AOP&#xff08;面向切面编程&#xff09;功能非常强大、易用。而它背后的字节码生成技术&#xff08;在运行时&#xff0c;根据需要修改和生成Java字节码的技术&#xff09;就是一项重要的支撑技术。 …

spring之AOP简单介绍

1.AOP的概念 AOP&#xff0c;Aspect Oriented Programming&#xff0c;面向切面编程&#xff0c;是对面向对象编程OOP的升华。OOP是纵向对一个 事物的抽象&#xff0c;一个对象包括静态的属性信息&#xff0c;包括动态的方法信息等。而AOP是横向的对不同事物的抽象&#xff0c;…

云原生之深入解析Redis的原理分析与环境部署

一、Redis 简介 ① 什么是 Redis &#xff1f; REmote DIctionary Server&#xff08;Redis&#xff09;是一个由 Salvatore Sanfilippo 写的 key-value 存储系统&#xff0c;是跨平台的非关系型数据库。Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可…

Linux —— 基础I/O(一)

目录 一&#xff0c;背景介绍 二&#xff0c;系统接口函数 open 打开或创建文件 close 关闭文件 read 读取文件 write 写入文件 三&#xff0c;文件描述符 结构体指针files 结构体指针files指向的表files_struct 文件结构体 一&#xff0c;背景介绍 狭义的文件存放在…

爆肝整理,接口测试-Fiddler对Jmeter请求抓包(详细实战)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 使用Fiddler结合J…

编写html页面让谷歌浏览器不弹出“翻译此页”的问题

文章目录 1.问题2.解决总结 1.问题 2.解决 如下图&#xff0c;将html的lang由en改为zh-CN&#xff08;注意大小写&#xff09; 总结 将html的lang由en改为zh-CN

基于PyTorch的图像识别

前言 图像识别是计算机视觉领域的一个重要方向&#xff0c;具有广泛的应用场景&#xff0c;如医学影像诊断、智能驾驶、安防监控等。在本项目中&#xff0c;我们将使用PyTorch来开发一个基于卷积神经网络的图像识别模型&#xff0c;用来识别图像中的物体。下面是要识别的四种物…

Java作业2

1.递归求解汉诺塔问题 拿三个为例子 先从A设法拿走两个盘子到B上&#xff0c;再拿一个盘子到C上&#xff0c;再从B上挪走一个到A&#xff0c;再挪下面的到C上 如果有N个盘子&#xff0c;我们也一样的步骤&#xff0c;先设法拿N-1个盘子到B&#xff0c;再拿最底部的盘子到C&a…

【算法】逆波兰表达式

文章目录 定义求法代码思想&#xff1a; 定义 逆波兰表达式也称为“后缀表达式”&#xff0c;是将运算符写在操作数之后的运算式。 求法 *如&#xff1a;(ab)c-(ab)/e的转换过程&#xff1a; 先加上所有的括号。 (((ab)*c)-((ab)/e))将所有的运算符移到括号外面 (((ab) c)* …

Unity游戏源码分享-全民飞机大战源码unity2019

Unity游戏源码分享-全民飞机大战源码unity2019 项目地址&#xff1a;https://download.csdn.net/download/Highning0007/88204008

67 # 对象的处理

上一节学习了 form 数据的处理&#xff0c;这一节学习 Ajax 的方式提交数据 服务端的代码如下 const http require("http"); const url require("url"); const querystring require("querystring");let server http.createServer();server…