数据结构之双链表的相关知识点及应用

 找往期文章包括但不限于本期文章中不懂的知识点:

个人主页:我要学编程(ಥ_ಥ)-CSDN博客

所属专栏:数据结构

目录

双链表的实现 

初始化双链表 

在双链表中尾插数据 

在双链表中尾删数据

在双链表中头插数据 

在双链表中头删数据 

在双链表中的指定位置之后插入数据 

在双链表中删除指定位置的数据

在双链表中查找指定位置

销毁双链表 

双链表源码 


学习完单链表后,就要开始学习链表中最重要的双链表了。

双链表是双向带头循环链表。与单链表是恰恰相反。

接下来就用双链表来实现一系列增删查改的功能。 

双链表的实现 

 在创建双链表之前,还得要做一些提起准备。创建三个文件:List.h  List.c  test.c  前面两个是实现双链表的,后面的 test.c 文件是测试双链表的各种功能。同样链表是由一个一个的节点组成的。我们就得由节点的结构。

创建节点:

typedef int LTDataType;typedef struct ListNode
{struct ListNode* next; //指针保存下⼀个节点的地址struct ListNode* prev; //指针保存前⼀个节点的地址LTDataType data;
}LTNode;

初始化双链表 

因为双链表带头,因此,我们就得创建一个哨兵位。这个函数也可以叫做初始化函数。

//初始化
void LTInit(LTNode** pphead)//注意这里需要改变哨兵位,因此用二级指针接收
{//创建一个新的节点LTNode* phead = (LTNode*)malloc(sizeof(LTNode));if (phead == NULL){perror("malloc:");exit(1);}//把哨兵位的数据初始化为-1(无效数据),后驱指针指向自己,前驱指针指向自己*pphead = phead;(*pphead)->data = -1;(*pphead)->next = (*pphead)->prev = *pphead;
}

只要是增加节点,就会有重复的代码因此我们分装成一个函数,并且我们在初始化函数也可以传我们想要设置的无效数据。

//增加节点
LTNode* LTBuyNode(LTDataType x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("malloc:");exit(1);}newnode->data = x;newnode->next = newnode->prev = newnode;return newnode;
}
//初始化
void LTInit(LTNode** pphead)
{*pphead = LTBuyNode(-1);
}

在双链表中尾插数据 

情况一:链表中只有哨兵位:

情况二:链表中不止有哨兵位:

我们要尾插数据,就是要d3的next指针的指向,还要改变head的prev指针的指向。此外还得把新增加的节点prev指针指向d3,next指向head。

不能改变顺序的原因:如果改变了,就先把哨兵位的指向改变了,后面我们就找不到原链表的尾节点了,除非能把原链表的尾节点的地址提前存起来。

//尾插数据
void LTPushBack(LTNode* phead, LTDataType x)//哨兵位已经确定,不再改变,因此用一级指针
{assert(phead);//链表不能为空LTNode* newnode = LTBuyNode(x);//开始尾插节点  //链表中只有哨兵位if (phead->next == phead){//先把新节点安排好newnode->next = phead;newnode->prev = phead;//哨兵位phead->next = newnode;phead->prev = newnode;}else{//先把新节点安排好newnode->next = phead;newnode->prev = phead->prev;//头节点:phead  尾节点:phead->prev   新节点:newnodephead->prev->next = newnode;phead->prev = newnode;}
}

写完之后,还得测试一下我们所写的代码是否正确:可以用打印函数来判断看看结果是否和我们的预期一样。

//打印数据
void LTPrint(LTNode* phead)
{//遍历寻找LTNode* pcur = phead->next;//头节点的数据是无效的,因此就不需要打印//因为是循环的,所以不能用空指针来判断,要看看是否指向的哨兵位while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("\n");
}

在双链表中尾删数据

情况一:链表中由多个有效数据:

情况二:链表中只有一个有效数据:

//尾删数据
void LTPopBack(LTNode* phead)
{assert(phead && phead->next != phead);//链表不能为空并且链表中不能没有有效元素if (phead->next->next == phead)//只有一个有效数据{LTNode* freenode = phead->next;free(freenode);phead->next = phead;phead->prev = phead;}else{phead->prev->prev->next = phead;LTNode* freenode = phead->prev;phead->prev = phead->prev->prev;free(freenode);freenode = NULL;}
}

其实上面的写法可以简化为:

//尾删数据
void LTPopBack(LTNode* phead)
{assert(phead && phead->next != phead);//链表不能为空并且链表中不能没有有效元素phead->prev->prev->next = phead;LTNode* freenode = phead->prev;phead->prev = phead->prev->prev;free(freenode);freenode = NULL;
}

也就是说不管链表的有效元素的个数有多少,都不影响。(把特殊情况带入这个简化版里判断就可以了)。

在双链表中头插数据 

情况一:链表中不止有哨兵位: 

之所以在哨兵位的前面插入数据叫作尾插,是因为双链表是循环的,当遍历到d3后就会找到前面的节点,这就是在尾部,因此也叫作尾插。 

同样顺序不能变的原因也是因为顺序一旦改变,先把phead的next指向给改变了,就找不到了要更改的数据了。

情况二:链表中只有哨兵位:

//头插数据
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);if (phead->next == phead)//只有哨兵位{newnode->next = phead;newnode->prev = phead;phead->next = newnode;phead->prev = newnode;}else{//先安排新节点newnode->next = phead->next;newnode->prev = phead;//头节点:phead  尾节点(相较于新节点):phead->prev  新节点:newnodephead->next->prev = newnode;phead->next = newnode;}
}

这个头插也是可以简化的:

//头插数据
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);//先安排新节点newnode->next = phead->next;newnode->prev = phead;//头节点:phead  尾节点(相较于新节点):phead->prev  新节点:newnodephead->next->prev = newnode;phead->next = newnode;
}

简化判断的方法就是把特殊情况带入进去,看看能否成功。

在双链表中头删数据 

//头删数据
void LTPopFront(LTNode* phead)
{assert(phead && phead->next != phead);//链表不为空并且链表的有效数据不能为空phead->next->next->prev = phead;LTNode* freenode = phead->next;phead->next = phead->next->next;free(freenode);freenode = NULL;
}

在双链表中的指定位置之后插入数据 

情况一:pos是哨兵位:

情况二:pos是中间位置:

情况三:pos是尾节点: 

上面的代码不管是在哪个位置都满足。

//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = LTBuyNode(x);//先安排好新节点newnode->prev = pos;newnode->next = pos->next;pos->next = newnode;pos->next->prev = newnode;
}

在双链表中删除指定位置的数据

 情况一:pos在中间位置

情况二:pos在结尾位置:

注意:这个指定位置不能是哨兵位。

//在pos位置删除数据
void LTErase(LTNode* pos)
{assert(pos);pos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);
}

在双链表中查找指定位置

这个也比较简单,就是直接遍历整个双链表,如果没找到就返回NULL,找到就返回这个地址。

//查找指定数据
LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead && phead->next != phead);//链表不能为空并且链表不能只有哨兵位LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}

销毁双链表 

就是把链表中的节点一个一个的释放空间就行了。

//销毁链表
void LTDestroy(LTNode* phead)
{assert(phead);//就是节点一个一个的销毁LTNode* pcur = phead->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}free(pcur);pcur = NULL;
}

综合上面的代码来看,还有一个地方有点小瑕疵,就是初始化链表时,我们用的是二级指针,为了保持接口一致性,我们要用一级指针或者不传参数。

//初始化
LTNode* LTInit()
{LTNode* pplist = LTBuyNode(-1);return pplist;
}

双链表源码 

下面就是双链表完整的源码:

List.c

#include "List.h"//增加节点
LTNode* LTBuyNode(LTDataType x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("malloc:");exit(1);}newnode->data = x;newnode->next = newnode->prev = newnode;return newnode;
}//初始化
LTNode* LTInit()
{LTNode* pplist = LTBuyNode(-1);return pplist;
}//尾插数据
void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);//开始尾插节点  //链表中只有哨兵位if (phead->next == phead){//先把新节点安排好newnode->next = phead;newnode->prev = phead;//哨兵位phead->next = newnode;phead->prev = newnode;}else{//先把新节点安排好newnode->next = phead;newnode->prev = phead->prev;//头节点:phead  尾节点:phead->prev   新节点:newnodephead->prev->next = newnode;phead->prev = newnode;}
}//打印数据
void LTPrint(LTNode* phead)
{//遍历寻找LTNode* pcur = phead->next;//头节点的数据是无效的,因此就不需要打印//因为是循环的,所以不能用空指针来判断,要看看是否指向的哨兵位while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("\n");
}//尾删数据
void LTPopBack(LTNode* phead)
{assert(phead && phead->next != phead);//链表不能为空并且链表中不能没有有效元素phead->prev->prev->next = phead;LTNode* freenode = phead->prev;phead->prev = phead->prev->prev;free(freenode);freenode = NULL;
}//头插数据
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);//先安排新节点newnode->next = phead->next;newnode->prev = phead;//头节点:phead  尾节点(相较于新节点):phead->prev  新节点:newnodephead->next->prev = newnode;phead->next = newnode;
}//头删数据
void LTPopFront(LTNode* phead)
{assert(phead && phead->next != phead);//链表不为空并且链表的有效数据不能为空phead->next->next->prev = phead;LTNode* freenode = phead->next;phead->next = phead->next->next;free(freenode);freenode = NULL;
}//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = LTBuyNode(x);//先安排好新节点newnode->prev = pos;newnode->next = pos->next;pos->next = newnode;pos->next->prev = newnode;
}//在pos位置删除数据
void LTErase(LTNode* pos)
{assert(pos);pos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);pos = NULL;
}//查找指定数据
LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead && phead->next != phead);//链表不能为空并且链表不能只有哨兵位LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}//销毁链表
void LTDestroy(LTNode* phead)
{assert(phead);//就是节点一个一个的销毁LTNode* pcur = phead->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}free(pcur);pcur = NULL;
}

List.h

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>typedef int LTDataType;typedef struct ListNode
{struct ListNode* next; //指针保存下⼀个节点的地址struct ListNode* prev; //指针保存前⼀个节点的地址LTDataType data;
}LTNode;//初始化
LTNode * LTInit();//销毁链表
void LTDestroy(LTNode* phead);//打印数据
void LTPrint(LTNode* phead);//尾插数据
void LTPushBack(LTNode* phead, LTDataType x);//尾删数据
void LTPopBack(LTNode* phead);//头插数据
void LTPushFront(LTNode* phead, LTDataType x);//头删数据
void LTPopFront(LTNode* phead);//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDataType x);//在pos位置删除数据
void LTErase(LTNode* pos);//查找指定数据
LTNode* LTFind(LTNode* phead, LTDataType x);

好啦!本期数据结构双链表的学习就到此为止啦!我们下一期再一起学习吧!

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

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

相关文章

低代码技术与仓储管理的新纪元:革命性的供应链变革

引言 在当今数字化时代&#xff0c;企业对于创新和效率的追求越发迫切。在这样的背景下&#xff0c;低代码技术应运而生&#xff0c;成为企业数字化转型的重要工具之一。低代码技术的崛起为企业提供了一种快速、灵活、成本效益高的开发方式&#xff0c;大大缩短了软件开发周期…

POJO,Entity,model,domain,view,DTO,VO,Param这些分别都是什么含义?怎样理解?

目录 1. 前言 2. POJO的含义 3. entity(实体) 4. model(模型) 5. domain(域) 6. view(视图) 7. DTO(数据传输对象) 8. VO(真正视图层) 9. Param(参数) 10. 总结 1. 前言 在日常开发的过程中&#xff0c;如果我们接手一个新的项目之后&#xff0c;通常会有各种各样的…

浅谈免杀下的持久化

文章目录 前记注册表计划任务COM劫持后记reference 前记 实战中持久化的手段常用的就是加服务、添改注册表、加计划任务、劫持等&#xff0c;这里探索c/c下的维权免杀 注册表 用户级 \HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run \HKEY_CURRENT_USER…

代码随想录算法训练营DAY36|C++贪心算法Part.5|435.无重叠区间、763.划分字母区间、56. 合并区间

文章目录 435.无重叠区间按右边界排序CPP代码 按左边界排序如何判断相邻区间是否重叠如何判断一下一个区间与当前相邻区间是否重叠总结CPP代码 763.划分字母区间思路伪代码实现CPP代码 56. 合并区间思路CPP代码 435.无重叠区间 力扣题目链接 文章链接&#xff1a;435.无重叠区间…

Python 0基础_变现_38岁_day 15(匿名函数)

匿名函数&#xff1a; 不用定义函数名&#xff0c;无需使用def关键字&#xff0c;使用lambda将函数写成一行&#xff1b;#使用匿名函数定义一个两个数字相加的函数add lambda x,y : xy #使用变量接收匿名函数的内容&#xff0c;且变量名作为调用函数的变量名&#xff1…

EasyRecovery数据恢复软件2025破解版安装包下载

EasyRecovery数据恢复软件的主要功能及使用教程。coco玛奇朵可以提供一个概要和简化的教程&#xff0c;以便你了解其基本内容和操作步骤。 EasyRecovery绿色破解下载网盘链接: https://pan.baidu.com/s/1_6NmcOh_Jmc-DGc4TJD-Mg?pwddq4w 提取码: dq4w 复制这段内容后打开百度…

强固型工业电脑在称重系统+叉车电脑,称重量体扫码一体机,物流分拣线工作站行业应用

称重系统叉车电脑行业应用 背景介绍 在叉车上安装称重传感器&#xff0c;通过对举升压力的自动检测&#xff0c;将压力信号转换为电流或电压信号&#xff0c;经过A/D转换&#xff0c;使模拟信号变为数字信号&#xff0c;经微处理器进行数据处理后通过蓝牙、串口或者USB接口将称…

dial tcp 192.168.0.190:443: connect: connection refused

1、场景 用nerdctl登录镜像仓库192.168.0.190&#xff08;Harbor&#xff09;&#xff0c;报错 ERRO[0006] failed to call tryLoginWithRegHost error"failed to call rh.Client.Do: Get \"https://192.168.0.190/v2/\": dial tcp 192.168.0.190:…

【数据结构】图(Graph)

文章目录 概念图的存储方式邻接矩阵邻接矩阵表示法邻接矩阵表示法的特点 邻接表邻接表表示法邻接表表示法的特点邻接表表示法的定义与实现查找插入删除其它构造函数析构函数创建图输出图 图的遍历深度优先遍历&#xff08;DFS&#xff09;广度优先遍历 图的连接分量和生成树生成…

C#设计树形程序界面的方法:创建特殊窗体

目录 1.TreeView控件 2.实例 &#xff08;1&#xff09;Resources.Designer.cs &#xff08;2&#xff09;Form1.Designer.cs &#xff08;3&#xff09;Form1.cs &#xff08;4&#xff09;生成效果 以树形来显示程序的菜单&#xff0c;可以更直观、更快捷地对窗体进行…

科技赋能无人零售

科技赋能无人零售&#xff0c;使其具备以下独特优势&#xff1a; 1. 全天候无缝服务 &#xff1a;无人零售店依托科技&#xff0c;实现24小时不间断运营&#xff0c;不受人力限制&#xff0c;满足消费者随时购物需求&#xff0c;尤其惠及夜间工作者、夜猫子及急需购物者&…

Android --- 常见UI组件

TextView 文本视图 设置字体大小&#xff1a;android:textSize"20sp" 用sp 设置颜色&#xff1a;android:textColor"#00ffff" 设置倍距(行距)&#xff1a;android:lineSpacingMultiplier"2" 设置具体行距&#xff1a;android:lineSpacingExtra&q…

基于RK3588的全国产鸿蒙边缘计算工控机在智能交通ETC收费系统的应用

1.1 产品简介 基于智能交通、工业互联等行业快速智能化发展的需求&#xff0c;以 OpenHarmony 为框架开发嵌入 HamonyOS&#xff0c;打造了具有高智能、高可靠、高安全的自主 可控的边缘处理器 XM-RK3588。 图 1-1 边缘处理器 HamonyOS强化 IoT 互联互动能力&#xff0c;让边缘…

Python爬虫入门指南--爬虫技术的由来、发展与未来--实战课程大赠送

爬虫&#xff0c;也称为网络爬虫或网络蜘蛛&#xff0c;是一种自动化程序&#xff0c;专门用于遍历互联网并收集数据。这种技术的起源、发展和未来都与互联网紧密相连&#xff0c;并在信息检索、数据挖掘等多个领域发挥着不可或缺的作用。 "免费IP池大放送&#xff01;助…

堆的概念、堆的向下调整算法、堆的向上调整算法、堆的基本功能实现

目录 堆的介绍 堆的概念 堆的性质 堆的结构 堆的向下调整算法 基本思想&#xff08;以建小堆为例&#xff09; 代码 堆的向上调整算法 基本思想&#xff08;以建小堆为例&#xff09; 代码 堆功能的实现 堆的初始化 HeapInit 销毁堆 HeapDestroy 打印堆 HeapPrint …

洛谷 P1021 邮票面值设计

原题链接&#xff1a;[NOIP1999 提高组] 邮票面值设计 - 洛谷 目录 题目描述 解题思路&#xff1a; 代码实现&#xff1a; 题后总结&#xff1a; 题目描述 给定一个信封&#xff0c;最多只允许粘贴 N 张邮票&#xff0c;计算在给定 K&#xff08;NK≤15&#xff09;种邮票…

RAG的进化之路:从单兵作战到多智协作

原文&#xff1a;https://arxiv.org/pdf/2404.15155.pdf 近年来,随着大规模预训练语言模型的蓬勃发展,基于检索的知识问答技术越来越受到学术界和工业界的青睐。其中最具代表性的当属RAG方法。RAG通过将外部知识库集成到语言模型中,对输入的问题进行深入理解、推理,并生成相应的…

【C语言】联合体详解

目录 1.联合体的声明 2.联合体的特点 3.相同成员的结构体和联合体对比 4.联合体大小的计算 1.联合体的声明 像结构体一样&#xff0c;联合体也是由一个或者多个成员构成&#xff0c;这些成员可以不同的类型。但是编译器只为最大的成员分配足够的内存空间。 联合体的特点是所…

骑砍2霸主MOD开发(6)-使用C#-Harmony修改本体游戏逻辑

一.C#-Harmony反射及动态注入 利用C#运行时环境的反射原理,实现对已加载DLL,未加载DLL中代码替换和前置后置插桩. C#依赖库下载地址:霸王•吕布 / CSharpHarmonyLib GitCodehttps://gitcode.net/qq_35829452/csharpharmonylib 根据实际运行.Net环境选择对应版本的0Harmony.dll…

C++(Qt)软件调试---crashpad捕获崩溃(19)

C(Qt)软件调试—crashpad捕获崩溃&#xff08;19&#xff09; 文章目录 C(Qt)软件调试---crashpad捕获崩溃&#xff08;19&#xff09;1、概述2、资源地址3、配置环境4、解决报错5、测试代码6、测试结果7、Qt中使用crashpad 更多精彩内容&#x1f449;个人内容分类汇总 &#x…