【数据结构】从头到尾全解析双向链表

news2024/11/30 4:58:08

在之前我们已经讲过< 单链表 >了,单链表查找上一个结点的时间复杂度为O(n),尾插时也要遍历一次链表也是O(n),因为我们每次都要从头开始遍历找,为了克服这单向性的缺点,我们就有了双向链表.
如果要提高链表的查找,尾插等效率,那双向链表(双链表)无疑是首选。

文章目录

  • 双向链表的概念及结构
  • 双向链表接口的实现
    • 申请节点空间
    • 双向链表的初始化
    • 双向链表打印数据
    • 双向链表是否为空
    • 双向链表尾插
    • 双向链表头插
    • 双向链表尾删
    • 双向链表头删
    • 双向链表的查找
    • 双向链表在指定位置前插入数据
    • 双向链表删除指定位置的值
    • 双向链表的销毁
  • 总结

双向链表的概念及结构

双向链表是一种常用的数据结构,它允许我们在O(1)时间内对链表的头尾进行元素的添加和删除操作,同时也支持双向遍历。

双向链表是在单链表的每个结点中,再设置一个指向其前驱结点的指针域,所以在双向链表中的结点都有两个指针域,一个指向直接后继,另一个指向直接前驱。
在这里插入图片描述

双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。


双向链表的结构:双向链表的每个节点包含了三个基本元素,分别是元素值、指向前一个节点的指针和指向下一个节点的指针。

  • 双向链表存储结构
struct SListNode
{
	int data;  //节点存储数据
	struct SListNode* next; //指向前驱节点
	struct SListNode* prev; //指向后驱节点
};
  • 注意:

双向链表头指针是一个虚拟头节点,不存储任何有效数据,他的前驱节点与后驱节点都是指向自己的,头指针节点相当于是一个削兵。用来站岗的。方便链接前后指针.


双向链表接口的实现

申请节点空间

  • 链表添加一个节点数据时候,每次都要写一段代码,这样做是不是太繁琐了,我们可以封装一个函数来解决问题,每次添加一个节点时,将结点存放的数据置为需要存放的值,在将结构体存放节点的地址置为NULL, 需要增加节点时调用一下改函数即可。
//申请节点空间
LTNode* BuyList(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)  //判断是否申请成功
	{
		perror("malloc fail");
	}
	else   //申请成功
	{
		newnode->data = x;  //将结点存放的数据置为需要存放的值
		newnode->next = NULL;  //节点的地址置为NULL
		newnode->prev = NULL;  //节点的地址置为NULL
		return newnode;
	}
}

双向链表的初始化

  • 其实在双向链表操作中,我们只需要一级指针接收就能修改链表的指向了,但在初始化时候我们要修改头节点指针,需要二级指针来接收修改头节点,为了后面统一用一级指针,所以我们在初始化时候不传参,直接申请一个节点,然后返回,该指点的前驱指针与后驱指针都是指向自己。节点数据不存储有效数据
LTNode* LTInit() 
{
	LTNode* phead = BuyLTNode(-11111);//节点数据不存储有效数据
	//前驱指针与后驱指针都是指向自己
	phead->next = phead;  
	phead->prev = phead;
	return phead; //返回该节点
}

双向链表打印数据

  • 双向链表是通过结构体存储的头指针下一个指针开始遍历链表的,当遍历的指针指向头指针则遍历完毕。
void LTPrint(LTNode* phead)
{
	assert(phead); //断言:判断是否为空

	printf("guard<==>");
	LTNode* cur = phead->next; //从头指针的next开始遍历
	while (cur != phead)
	{
		printf("%d<==>", cur->data); // 打印数据
		cur = cur->next; //迭代指向下一个
	}
	printf("\n");
}

双向链表是否为空

  • 如果phead->next为自己,则链表为空,返回真,反之返回假.
bool LTEmpty(LTNode* phead)
{
	assert(phead); //断言 判断是否为空指针

	return phead->next == phead; 
}

双向链表尾插

  • 双向链表尾插步骤
    1. 创建一个新节点 ,将新节点的数据填充为要插入的数据。
    2. 使用phead->prev找到当前双向链表的尾节点tail。
    3. 将tail节点的next指针指向新的待插入节点newnode
    4. 将待插入节点newnode的prev指针指向原尾节点tail
    5. 将待插入节点newnode的next指针指向链表的头节点phead。
    6. 将链表头节点phead的prev指针指向插入的新节点newnode。
// 双向链表尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead); //断言:判断是否为空
	LTNode* tail = phead->prev;  //使用phead->prev找到当前双向链表的尾节点tail。
	LTNode* newnode = BuyLTNode(x); //创建一个新节点 

	tail->next = newnode;  //将tail节点的next指针指向新的待插入节点newnode
	newnode->prev = tail;  //将待插入节点newnode的prev指针指向原尾节点tail
	newnode->next = phead; //将待插入节点newnode的next指针指向链表的头节点phead。
	phead->prev = newnode; //将链表头节点phead的prev指针指向插入的新节点newnode。
}

双向链表头插

  • 双向链表头插步骤
    1. 创建一个新节点 ,将新节点的数据填充为要插入的数据。
    2. 使用phead->next找到当前双向链表的头节点,用first保存.
    3. 将链表头节点phead的next指针指向新插入的节点newnode
    4. 将待插入节点newnode的prev指针指向链表的头节点phead。
    5. 将待插入节点newnode的next指针指向原头节点first。
    6. 将原头节点first的prev指针指向插入的新节点newnode。
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead); //断言 判断是否为空
	LTNode* newnode = BuyLTNode(x); //创建一个新节点 ,将新节点的数据填充为要插入的数据
	LTNode* first = phead->next; //使用phead->next找到当前双向链表的头节点

	phead->next = newnode;  //将链表头节点phead的next指针指向新插入的节点newnode
	newnode->prev = phead;  //将待插入节点newnode的prev指针指向链表的头节点phead。

	newnode->next = first;  //将待插入节点newnode的next指针指向原头节点first。
	first->prev = newnode;  //将原头节点first的prev指针指向插入的新节点newnode。
}

双向链表尾删

  • 双向链表尾删步骤
    1. 判断链表是否为空,如果为空,则无法进行删除操作
    2. 使用phead->prev找到当前双向链表的尾节点.(用一个临时变量tail来记录)
    3. 使用tail->prev找到当前尾节点的前一个节点。(用一个临时变量tailPrev来记录)
    4. 释放尾节点tail的内存空间
    5. 将tailPrev的next指针指向链表头节点phead。
    6. 将链表头节点phead的prev指向tailPrev
void LTPopBack(LTNode* phead)
{
	assert(phead);  //判断头指针是否为空
	assert(!LTEmpty(phead)); //断言:判断链表是否为空,如果为空,则无法进行删除操作
	LTNode* tail = phead->prev;    //使用phead->prev找到当前双向链表的尾节点.
	LTNode* tailPrev = tail->prev; //使用tail->prev找到当前尾节点的前一个节点

	free(tail); //释放尾节点tail的内存空间
	tailPrev->next = phead; //将tailPrev的next指针指向链表头节点phead
	phead->prev = tailPrev; //将链表头节点phead的prev指向tailPrev

}

双向链表头删

  • 双向链表头删步骤
    1. 判断链表是否为空,如果为空,则无法进行删除操作
    2. 使用phead->next找到当前双向链表的头节点下一个。(用一个临时变量first来记录)
    3. 使用first->next找到当前头节点的下一个节点(用一个临时变量second来记录)
    4. 将链表头节点phead的next指针指向second节点。
    5. 将second节点的prev指针指向链表头节点phead。
    6. 释放头节点first的内存空间,进行头删.
void LTPopFront(LTNode* phead)
{
	assert(phead); 判断头指针是否为空
	assert(!LTEmpty(phead)); 断言:判断链表是否为空,如果为空,则无法进行删除操作

	LTNode* first = phead->next;  //使用phead->next找到当前双向链表的头节点的下一个
	LTNode* second = first->next; //使用first->next找到当前头节点的下一个节点

	phead->next = second;  //将链表头节点phead的next指针指向second节点
	second->prev = phead;  //将second节点的prev指针指向链表头节点phead

	free(first); //进行头删
}

双向链表的查找

  • 获得链表某个节点的数据思路也较简单
    1. 从头节点的下一个节点开始,依次遍历链表中的每个节点。
    2. 在每个节点的数据域中查找节点的值是否为x,如果是则返回该节点的指针
    3. 如果遍历完整个链表都没有找到节点的值为x,则返回NULL。
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead); ///判断头指针是否为空

	LTNode* cur = phead->next; //从头节点的下一个节点开始,依次遍历链表中的每个节点。
	while (cur != phead)
	{
		if (cur->data == x) //在每个节点的数据域中查找节点的值是否为x,如果是则返回该节点的指针
		{
			return cur;
		}

		cur = cur->next; //迭代 指向下一个
	}

	return NULL; //如果遍历完整个链表都没有找到节点的值为x,则返回NULL。
}

双向链表在指定位置前插入数据

  • 双向链表插入数据不用像单链表一样从头查找,双向链表只需原地动数据就可。具体步骤如下:
    1. 获取pos节点的前一个节点(使用一个临时变量tmp来保存)
    2. 创建一个新的待插入节点newnode
    3. 将tmp节点的next指向待插入节点newnode
    4. 将待插入节点newnode的prev指针指向tmp节点
    5. 将待插入节点newnode的next指针指向pos节点
    6. 将pos节点的prev指针指向待插入节点newnode
// 在pos之前插入
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);//节点pos不为空

	LTNode* tmp = pos->prev; //获取pos节点的前一个节点
	LTNode* newnode = BuyLTNode(x); //创建一个新的待插入节点newnode

	tmp->next = newnode; //将tmp节点的next指向待插入节点newnode
	newnode->prev = tmp; //将待插入节点newnode的prev指针指向tmp节点
	newnode->next = pos;  //将待插入节点newnode的next指针指向pos节点
	pos->prev = newnode;  //将pos节点的prev指针指向待插入节点newnode
}

双向链表删除指定位置的值

  • 删除指定位置的具体逻辑步骤
    1. 获取pos节点的前一个节点和后一个节点。(用posprev和posnext临时变量来记录)
    2. 将posPrev节点的next指向posNext节点
    3. 将posNext节点的prev指向posPrev节点
    4. 释放要删除的节点pos的内存空间。
void LTErase(LTNode* pos)
{
	assert(pos); //节点pos不为空
	
    //获取pos节点的前一个节点和后一个节点
	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;

	posPrev->next = posNext;  //将posPrev节点的next指向posNext节点
	posNext->prev = posPrev;  //将posNext节点的prev指向posPrev节点
	free(pos);  //释放要删除的节点pos的内存空间。
}

双向链表的销毁

  • 当我们不打算使用这个单链表时,我们需要把它销毁,其实也就是在内存中将它释放掉,以便于留出空间给其他程序或软件使用。释放内存具体步骤如下:
    1. 使用一个指针变量遍历整个链表在循环中,对于当前遍历到的节点,将其保留下一个节点的指针,以便于在释放当前指针后,指向的节点时能够顺利找到下一个节点.
    2. 先从头指针的下一个节点开始释放,最后在释放头指针.
void LTDestroy(LTNode* phead)
{
	assert(phead); //断言:头指针不能为空

	LTNode* cur = phead->next;//先指向有效位置头指针的下一个节点
	while (cur != phead)
	{
		LTNode* next = cur->next; //保存下一个指针
		free(cur); //释放cur节点占用的内存空间,
		cur = next; //迭代
	}

	
	free(phead); //释放头指针
}

总结

  • 双向链表相对于单向链表的优点在于它可以支持双向遍历,即从链表头或尾部开始遍历,因此它可以在某些情况下更加高效。同时,双向链表也支持在任意位置进行链表节点的插入和删除操作,这是单向链表不支持的。因此,双向链表更加灵活,可以满足更多场景下的需求。
  • 相对于单链表,双向链表的节点需要额外存储一个指针,即指向前面节点的指针,因此相对于单链表会占用更多的内存空间。

总之,双向链表是一种值得掌握的重要数据结构,掌握了它的基本操作和应用场景,可以大大提高算法和数据结构等领域的编程水平。

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

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

相关文章

软件测试专业应届生应如何提高职场竞争力

一&#xff1a;巩固专业知识 背景&#xff1a;笔者已经做了几年的打工人&#xff0c;以个人经验给软件测试专业应届生一些建议。 推荐需要掌握的知识&#xff1a; 1、软件测试基础知识&#xff08;软件生命周期每个阶段工作需了解&#xff09; 2、熟悉SQL/MySQL/Oracle数据库&…

D8加密狗使用教程

D8 加密锁 1.VsCode 安装中文扩展包(1) 打开 VsCode&#xff0c;点击左侧扩展.(2) 输入Chinese&#xff0c;会自动搜索&#xff0c;点击第一个中文简体扩展&#xff0c;点击安装(3) 重启VsCode 2. D8调试服务程序 - 只运行3. 自动安装 yttool&#xff08;1&#xff09;VsCode 打…

杭钢集团:以用友iuap为数智底座的数智化转型之路

近日&#xff0c;一年一度的用友BIP技术大会圆满召开。来自行业领先企业的CIO/CDO、生态伙伴、开发者、分析师、媒体等共聚北京用友产业园&#xff0c;了解最新技术发展趋势、探讨行业热点话题。会上&#xff0c;杭钢集团总经理助理施永益分享了杭钢集团基于用友BIP-iuap平台推…

three.js学习 11 - threejs常用几何体,与几何体材质如何自定义

1.缓冲几何体&#xff08;立方体&#xff09; 官网API地址&#xff1a;https://www.three3d.cn/docs/index.html?qgeometry#api/zh/geometries/BoxGeometry 2.圆缓冲几何体 官网地址&#xff1a;https://www.three3d.cn/docs/index.html?qgeometry#api/zh/geometries/Cir…

ppt怎么压缩文件大小?试试这几种方法

ppt怎么压缩文件大小&#xff1f; PPT&#xff0c;也就是Microsoft PowerPoint演示文稿&#xff0c;是一种用于创建和展示演示文稿的软件工具。PPT的作用非常广泛。它可以用于各种场景中&#xff0c;以呈现信息、表达观点和传递思想。PPT可以将文字、图片、图表、动画等多种媒体…

程序员之间拉开差距最大的因素

本文为小红花技术领袖俱乐部创始人赖勇浩为《编程卓越之道》&#xff08;卷1&#xff09;所作序言。 拿到新一版《编程卓越之道》的第一卷《深入理解计算机》的稿子&#xff0c;心里非常感慨&#xff1a;上次读这本书&#xff0c;已经是16年前&#xff0c;还留下了幼稚的读后感…

1072. 按列翻转得到最大值等行数(leetcode,哈希)-------------------c++实现

1072. 按列翻转得到最大值等行数&#xff08;leetcode,哈希&#xff09;-------------------c实现 题目表述 给定 m x n 矩阵 matrix 。 你可以从中选出任意数量的列并翻转其上的 每个 单元格。&#xff08;即翻转后&#xff0c;单元格的值从 0 变成 1&#xff0c;或者从 1 …

排序算法之基数排序

一、基数排序&#xff08;RadixSort&#xff09; 基数排序&#xff08;Radix sort&#xff09;是一种非比较型整数排序算法。 1. 基本思想 原理是将整数按位数切割成不同的数字&#xff0c;然后按每个位数分别比较。基数排序的方式可以采用LSD&#xff08;Least significant…

超详细:阿里云服务器安装宝塔面板教程(需要开端口)

使用阿里云服务器安装宝塔面板教程&#xff0c;阿里云服务器网以CentOS操作系统为例&#xff0c;安装宝塔Linux面板&#xff0c;先远程连接到云服务器&#xff0c;然后执行宝塔面板安装命令&#xff0c;系统会自动安装宝塔面板&#xff0c;安装完成后会返回面板地址、账号和密码…

理论力学专题----拉普拉斯一龙格一楞次矢量

质量 m平方反比的有心力场 对应势能 V-k/r牛顿定律&#xff1a;\dot{\vec{p}}-k\frac{\vec{r}}{r^3} 空间旋转对称群 略 下学期学完了补上 LRL矢量 LRL矢量\vec{A}: define: \vec{A} \eqv \vec{p} \times \vec{L} - mk\frac{\vec{r}}{r} LRL守恒 \frac{d}{dt}(\vec{p} \…

图神经网络:(节点分类)在Cora数据集上动手实现图神经网络

文章说明&#xff1a; 1)参考资料&#xff1a;PYG官方文档。超链。 2)博主水平不高&#xff0c;如有错误还望批评指正。 3)我在百度网盘上传了这篇文章的jupyter notebook。超链。提取码8888。 文章目录 代码实操1&#xff1a;GCN的复杂实现代码实操2&#xff1a;GCN的简单实现…

从零开始Vue3+Element Plus的后台管理系统(二)——Layout页面布局的实现

项目搭建好之后&#xff0c;开始写基本的布局。后台管理系统的布局3大元素&#xff1a;头部、侧栏、主要内容&#xff0c;各种布局结构相差不大&#xff0c;我选择了下图所示的布局&#xff0c;其中头部、侧栏、页签在页面中是固定的&#xff0c;只有主要内容容器会跟随页面滚动…

如何从计算机或 SD 卡中恢复已删除的音乐文件?

与我们中的许多人一样&#xff0c;您可能已经从喜爱的专辑中下载并保存了多个音乐文件以供离线收听&#xff0c;但如果您不小心或意外删除了这些音乐文件怎么办&#xff1f;不用担心&#xff0c;我们在这里列出了几种从计算机或 SD 卡中恢复已删除或丢失的音乐文件的方法。 您…

001+limou+Git的安装与入门

0.前言 您好&#xff0c;这里是limou3434的一篇个人博文&#xff0c;感兴趣的话您也可以看看我的其他文章。本系列主要深入讲解有关Git的基础知识和基础使用&#xff0c;在文章中会结合部分Git网站上推荐的电子书《Pro Git》来对Git进行解读&#xff0c;意在补充书中对您“不友…

Java 面试 | RabbitMQ(2023版)

文章目录 rabbitmq1、为什么要使用rabbitmq2、rabbitmq如何确保消息发送?消息接收?3、RabbitMQ的构造4、Exchange交换器的类型5、RabbitMQ的持久化6、RabbitMQ消息发送和接收过程7、如何保证消息队列的高可用8、如何处理消息丢失的情况9、如何保证消息没有重复消费10、如何保…

Shell系统编程三剑客之----sed编辑器

目录 一:sed编辑器 1.sed编辑器概述 2.sed的工作流程 3.sed的命令格式 4.常用选项 5.常用操作 二&#xff1a;sed操作事例 1.查询 &#xff08;1&#xff09;打印内容 ​&#xff08;2&#xff09;打印行数 ​&#xff08;3&#xff09;打印特殊字符、ASCII码 &…

python爬虫简述

Python爬虫是一种自动化获取互联网数据的技术&#xff0c;它可以通过编写程序自动访问网站并抓取所需的数据。在本文中&#xff0c;我们将介绍Python爬虫的基础知识、常用库和实际应用。 一、Python爬虫的基础知识 爬虫的定义 爬虫是一种自动化获取互联网数据的技术&#xf…

屏幕录像怎么录?分享3个简单实用的方法!

案例&#xff1a;怎么录制电脑屏幕&#xff1f; 【对于我这种不太熟悉电脑的人来说&#xff0c;想要录制电脑屏幕十分困难。听说录制电脑屏幕&#xff0c;需要用到录屏工具。有没有小伙伴有好的录屏软件介绍&#xff0c;顺便附带一下教程&#xff01;求&#xff01;】 屏幕录…

【冶金轧钢、电厂 JL-8B/E集成电路电流继电器 CMOS运算 JOSEF约瑟】

JL-8B/E集成电路电流继电器名称:集成电路电流继电器型号:JL-8B/E触点容量250V5A功率消耗&#xff1c;5W返回系数过电流:0.90.97;欠电流:1.051.15整定范围0.03~60A 系列型号&#xff1a; JL-8A/E集成电路电流继电器; JL-8B/E集成电路电流继电器&#xff1b; JL-8A/E11-004集成电…

[离散数学]命题逻辑与推理

目录 主析取范式 主合取范式推理理论(假设前提条件为真推出的结论)真值表法直接证明法** 常用推理公式 ** 间接证明 CP规则--附加前提证明法&#xff0c;证明比较方便 单条件形式&#xff0c;提取前件间接法 归谬法 结论是单命题&#xff0c;取反前提引入 常用 latex 定义 主析…