双向链表(带头双向循环链表)巨详解!!!

news2024/11/25 4:40:41

概念

本文讲述的双向链表,全名叫做带头双向循环链表我们学习的链表总共有八种

在前文讲述单链表时所讲到的单链表,其实就叫做不带头单向不循环链表,这里的带头、不带头才是真正的头结点,前文中的头结点其实叫做首元素结点,为了方便理解就叫做头结点,要注意分别

那真正的头结点是什么呢?有什么用呢?

带头链表中头结点其实叫做“哨兵位”,在哨兵位中不存储任何有效元素,在这里就是占个位置,听起来有点占着茅坑不拉屎的意思,其实不然,它在循环链表中具有重要作用,接下来为你缓缓讲述

双向链表的实现

结构体形式

在本文讲述的双向链表中,它由三个部分组成:存储的数据(data)、指向上一结点的指针(prev)、指向下一结点的指针(next

代码为:

typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;//指向下一个结点地址的指针
	struct ListNode* prev;//指向上一个结点地址的指针

}LTNode;

初始化

双向链表的初始化有两种方法,一种是传参、一种是返回值形式

这里我们使用返回值形式,在初始化中为了实现循环,一开始要将它的next和prev都指向它本身,它存储的数据随便赋一个值这里我是赋值为-1

代码为:

LTNode* LTInit()
{
	LTNode* phead = (LTNode*)malloc(sizeof(LTNode));//申请空间建立结点--哨兵位
	phead->data = -1;
	phead->next = phead->prev = phead;
	
	return phead;
}

 结点的建立

代码为

LTNode* LTBuyNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	node->data = x;
	node->next = node->prev = node;
	return node;
}

打印

打印开始是从哨兵位的下一结点,结束条件是再次回到哨兵位

代码为

//打印
LTNode* LTPrint(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;//指向哨兵位的下一个结点
	while (pcur != phead)//循环结束条件:当pcur回到哨兵位时
	{
		printf("%d -> ", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

尾插

双向链表的尾部插入需要注意插入位置前后两个结点,本文求的链表是双向带头循环的,尾部插入就需要注意哨兵位和哨兵位前一个结点(也就是尾结点)

画图可得

代码为

//尾插
LTNode* LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	//phead phead->prev newnode
	newnode->next = phead;
	newnode->prev = phead->prev;

	phead->prev->next = newnode;
	phead->prev = newnode;
}

 头插

头插和尾插思路差不多,不同的就是根据插入位置,要处理的结点也不同,头部插入要注意的就是哨兵位和首元素结点(又是哨兵位的下一结点)

画图可得

代码为

//头插
LTNode* LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	//phead newnode phead->next
	newnode->next = phead->next;
	newnode->prev = phead;

	phead->next->prev = newnode;
	phead->next = newnode;
}

判空

判定链表是否只有哨兵位

代码为

//判空
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	return phead->next == phead;//当哨兵位的下一结点等于它自身时,链表为空
}

 尾删

尾部的删除要考虑的结点就是哨兵位和尾结点的前一结点

画图可得

代码为

//尾删
void LTPopBack(LTNode* phead)
{
	assert(!LTEmpty(phead));
	//phead del->prev del 
	LTNode* del = phead->prev;
	del->prev->next = phead;
	phead->prev = del->prev;
	free(del);
	del = NULL;
}

 头删

头部的删除就是处理哨兵位和首结点下一结点的关系

画图可得

代码为

//头删
void LTPopFront(LTNode* phead)
{
	assert(!LTEmpty(phead));
	//phead del del->next
	LTNode* del = phead->next;
	del->next->prev = phead;
	phead->next = del->next;
	free(del);
	del = NULL;
}

 找寻

找寻和单链表没什么区别,唯一要考虑的是,是从哨兵位的下一结点开始找寻,循环结束条件为再次回到哨兵位置

代码为

//找寻数据
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}

在指定位置之后插入数据

在给定位置之后插入数据,首先要将插入位置之后的结点位置保存下来,在进行结点的插入

画图可得

代码为

//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = LTBuyNode(x);
	LTNode* next = pos->next;
	//pos newnode pos->next(next)
	newnode->next = next;
	newnode->prev = pos;

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

在指定位置删除数据

这一步也很简单,不需要特意考虑哨兵位的关系,就直接将指定位置前后结点相连,再将指定位置结点删除就行

代码为

void LTErase(LTNode* pos)
{
	assert(pos);
	//pos->prev pos pos->next
	pos->next->prev = pos->prev;
	pos->prev->next = pos->next;
	free(pos);
	pos = NULL;

}

销毁

销毁有两种方式:

一种是以二级指针将链表在销毁函数中彻底实现销毁,但这种方法用到二级指针和其他功能实现函数用到的一级指针不同,这样会导致接口不一致,虽然不是什么大问题,如果别人要使用你的这个双向链表可能会出错,所以不建议使用

代码为

void LTDestroy(LTNode** pphead)//为了保证接口一致性,不使用这种方法
{
	assert(pphead);
	LTNode* pcur = (*pphead)->next;
	while (pcur != *pphead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(*pphead);
	*pphead = NULL;

}

第二种就是用一级指针,在销毁函数将除哨兵位以外的结点都给销毁,但是哨兵位要在函数外进行手动销毁,这种方式保证了接口的一致性

代码为

//销毁
void LTDestroy(LTNode* phead)
{
	assert(phead);
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	free(phead);
	phead = NULL;
}


链表和顺序表的区别

不同点顺序表链表
存储空间上物理结构为线性,为连续性逻辑结构上为线性,物理结构上不为线性
随机访问

支持(时间复杂度O(1))

可以根据下标进行随机访问

不支持(访问时间复杂度O(n))
任意位置插⼊或者删除元素
可能需要搬移元素,效率低O(N)
只需修改指针指向
插入动态顺序表在空间不足时可以申请空间,但可能会造成空间浪费不需要扩容,可以根据需求来进行空间的申请,不会造成空间浪费
应用场景元素高度存储和频繁访问在任意位置高效插入和删除数据

 根据这两种数据结构的对比,我们可以知道数据结构没有绝对的谁好谁坏,正所谓:存在即合理
不同的数据结构适用于不同的应用场景当中,就让我们继续学习更多的数据结构吧!!!

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

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

相关文章

时序预测 | Python基于CNN-transformer时间序列预测

时序预测 | Python基于CNN-transformer时间序列预测 目录 时序预测 | Python基于CNN-transformer时间序列预测预测效果基本介绍参考资料 预测效果 基本介绍 时序预测 | Python基于CNN-transformer时间序列预测 Cnn-transformer-自适应稀疏自注意力ASSA-对比归一化contranorm预…

网站架构知识之Ansible进阶(day022)

1.handler触发器 应用场景:一般用于分发配置文件时候,如果配置文件有变化,则重启服务,如果没有变化,则不重启服务 案列01:分发nfs配置文件,若文件发生改变则重启服务 2.when判断 用于给ans运…

Yolo11改进策略:上采样改进|CARAFE,轻量级上采样|即插即用|附改进方法+代码

论文介绍 CARAFE模块概述:本文介绍了一种名为CARAFE(Content-Aware ReAssembly of FEatures)的模块,它是一种用于特征上采样的新方法。应用场景:CARAFE模块旨在改进图像处理和计算机视觉任务中的上采样过程&#xff0…

常用的c++特性-->day02

c11新特性 可调用对象案例分析 可调用对象包装器语法案例可调用对象包装器作为函数参数补充:类型转换运算符案例 可调用对象绑定器语法格式绑定非类成员函数/变量案例1案例2案例3案例4 绑定类成员函数/变量 lambda表达式捕获列表案例1返回值案例2 --> 包装器绑定…

锐捷技能大赛—L2TP隧道与L2TP Over IPSec嵌套,并在隧道内运行OSPF

目录 总部与分支站点之间建立隧道 基础配置 配置L2TP VPN ​编辑配置L2TP Over IPSec L2TP Over IPSec隧道内运行OSPF协议 总部与分支站点之间建立隧道 拓扑如下 基础配置 R1 int g0/1 ip add 10.1.1.1 30 int g0/0 ip add 192.168.10.254 24 exit ip route 0.0.0.0 0.0…

python可视化将多张图整合到一起(画布)

这周有点事忙着,没时间重温刚结束的Mathurcup数学建模,这两天也是再看了下,论文还是赶紧挺烂的,但比国赛又有进步(说起国赛又不得不抱怨了,基本其余省份都发了,但江西......哎)。哎&…

网络编程、UDP、TCP、三次握手、四次挥手

一、初识网络编程 网络编程的概念:在网络通信协议下,不同计算机上运行的程序,进行的数据传输。 应用场景:即时通信、网游对战、金融证券、国际贸易、邮件等等。 不管是什么场景,都是计算机和计算机之间通过网络进行…

在 Jupyter Notebook 中使用 Matplotlib 进行交互式可视化的教程

在 Jupyter Notebook 中使用 Matplotlib 进行交互式可视化的教程 引言 数据可视化是数据分析的重要组成部分,能够帮助我们更直观地理解数据。Matplotlib 是 Python 中最流行的绘图库之一,而 Jupyter Notebook 则是进行数据分析和可视化的理想环境。本文…

[单例模式]

[设计模式] 设计模式是软件工程中的一种常见做法, 它可以理解为"模板", 是针对一些常见的特定场景, 给出的一些比较好的固定的解决方案. 不同语言适用的设计模式是不一样的. 这里我们接下来要谈到的是java中典型的设计模式. 而且由于设计模式比较适合有一定编程经…

STM32软件开发 —— STM32CudeMX使用优点

目 录 STM32CudeMX使用思路步骤详细 STM32CudeMX 在图形化工具STM32CudeMX出现之前,开发者通常是参考库驱动文件中的例程来配置芯片的,进行拷贝和修改等,为了提高开发效率,ST公司开发了STM32CudeMX工具,通过它简化了芯…

江西省补贴性线上职业技能培训管理平台(刷课系统)

江西省补贴性线上职业技能培训管理平台(刷课系统) 目的是为了刷这个网课 此系统有两个版本一个是脚本运行,另外一个是可视化界面运行 可视化运行 技术栈:flask、vue3 原理: 通过分析网站接口,对某些接口加密的参数进行逆向破解,从而修改请求…

Golang | Leetcode Golang题解之第546题移除盒子

题目: 题解: func removeBoxes(boxes []int) int {dp : [100][100][100]int{}var calculatePoints func(boxes []int, l, r, k int) intcalculatePoints func(boxes []int, l, r, k int) int {if l > r {return 0}if dp[l][r][k] 0 {r1, k1 : r, k…

es自动补全(仅供自己参考)

elasticssearch提供了CompletionSuggester查询来实现自动补全功能。这个查询会匹配以用户输入内容开头的词条并返回。为了提高补全查询效率,对于文档中字段的类型有一些约束: 查询类型必须是:completion 字段内容是多个补全词条形成的数组 P…

ANDROIDWORLD: A Dynamic Benchmarking Environment for Autonomous Agents论文学习

这个任务是基于androidenv的。这个环境之前学过,是一个用来进行强化学习的线上环境。而这篇文章的工作就是要给一些任务加上中间的奖励信号。这种训练环境的优点就是动态,与静态的数据集(比如说我自己的工作)不同,因此…

VBA10-处理Excel的动态数据区域

一、end获取数据边界 1、基本语法 1-1、示例: 2、配合row和column使用 2-1、示例1 2-2、示例2 此时,不管这个有数值的区域,怎么增加边界,对应的统计数据也会跟着变的! 二、end的缺陷 若是数据区域不连贯,则…

【FFmpeg】FFmpeg 函数简介 ③ ( 编解码相关函数 | FFmpeg 源码地址 | FFmpeg 解码器相关 结构体 和 函数 )

文章目录 一、FFmpeg 解码器简介1、解码流程分析2、FFmpeg 编解码器 本质3、FFmpeg 编解码器 ID 和 名称 二、FFmpeg 解码器相关 结构体 / 函数1、AVFormatContext 结构体2、avcodec_find_decoder 函数 - 根据 ID 查找 解码器3、avcodec_find_decoder_by_name 函数 - 根据 名称…

Linux完结

学习视频笔记均来自B站UP主" 泷羽sec",如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负 【linux基础之病毒编写(完结)】 https://www.bilibili.com/video…

分享三个python爬虫案例

一、爬取豆瓣电影排行榜Top250存储到Excel文件 近年来,Python在数据爬取和处理方面的应用越来越广泛。本文将介绍一个基于Python的爬虫程序,用于抓取豆瓣电影Top250的相关信息,并将其保存为Excel文件。 获取网页数据的函数,包括以…

【计网】数据链路层笔记

【计网】数据链路层 数据链路层概述 数据链路层在网络体系结构中所处的地位 链路、数据链路和帧 链路(Link)是指从一个节点到相邻节点的一段物理线路(有线或无线),而中间没有任何其他的交换节点。 数据链路(Data Link)是基于链路的。当在一条链路上传送数据时&a…

docker 拉取MySQL8.0镜像以及安装

目录 一、docker安装MySQL镜像 搜索images 拉取MySQL镜像 二、数据挂载 在/root/mysql/conf中创建 *.cnf 文件 创建容器,将数据,日志,配置文件映射到本机 检查MySQL是否启动成功: 三、DBeaver数据库连接 问题一、Public Key Retrieval is not allowed 问题…