数据结构与算法:双向链表

朋友们大家好啊,在上节完成单链表的讲解后,我们本篇文章来对带头循环双向链表进行讲解

双向链表

  • 双向链表、头节点和循环的介绍
  • 构建双向链表
    • 节点的构建
    • 初始化双向循环链表(空链表)
    • 销毁双向链表
  • 链表的打印
  • 双向链表头尾的插与删
    • 尾插
    • 尾删
    • 头插
    • 头删
  • 查找特定节点
    • 在指定位置前插入数据
    • 删除pos节点
  • 总结

双向链表、头节点和循环的介绍

在这里插入图片描述
单链表中,一个节点存储数据和指向下一个节点的指针,而双向链表除了上述两个内容,还包括了指向上一个节点的指针
在这里插入图片描述

带头的双向链表,是指在双向链表的最前端添加了一个额外的节点,这个节点被称为头节点(哨兵节点),但它一般不用于存储实际的数据(或者可以说存储的数据不被使用)。头节点的主要目的是为了简化链表操作的逻辑,避免在处理链表的开始和结束位置时需要进行特殊的条件判断

在没有头节点的普通双向链表中,如果链表为空,则链表的第一个节点(head pointer)直接为NULL,这使得插入和删除操作时,需要分别检查特定情况,如链表是否为空、是否在链表开始或结束位置进行操作等。

循环链表,即最后一个节点指向下一个节点的指针并不指向空,而是指向头结点,且头结点的指向上一个节点的指针也并不指向空,而是指向最后一个节点

简单介绍之后,我们就来讲解双向循环链表的各个细节吧

构建双向链表

typedef int LTDatatype;typedef struct ListNode
{struct ListNode* next;struct ListNode* prev;LTDatatype val;
}LTNode;

这里typedef int LTDatatype;我们多次提到,为类型抽象

构建的节点中,每个节点包括两个指针:

  • struct ListNode* next;
    这是一个指针,指向下一个ListNode节点。在链表中,每个节点通过这样的next指针连接到下一个节点。对于链表的最后一个节点,这个指针通常设为NULL,表示没有后续节点。但在循环链表的情况下,最后一个节点的next指针会指向链表的第一个节点,形成一个闭环。

  • struct ListNode* prev;
    这是另一个指针,指向前一个ListNode节点。在双向链表中,除了能够向前遍历,我们还可以通过这个prev指针向后遍历链表。对于链表的第一个节点,这个指针在非循环链表中通常设为NULL,表示没有前驱节点**。而在循环链表中,第一个节点的prev指针会指向链表的最后一个节点。**

节点的构建

我们首先定义一个函数

LTNode* CreatNode(LTDatatype x)

与单链表不同的是,这个函数多了一个指向前一个节点的指针,其他内容均相同

LTNode* CreatNode(LTDatatype x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("malloc fail");exit(-1);}newnode->val = x;newnode->next = NULL;newnode->prev = NULL;return newnode;
}

初始化双向循环链表(空链表)

在双向循环链表中,空链表的标志性质是其头节点的 next 和 prev指针都指向它自身。即使是空的链表,依然保持着循环的特性,但它不包含任何数据节点,只有这一个特殊的头节点

这里有两种初始化的形式

void LTInit(LTNode** phead)
{*phead = (LTNode*)malloc(sizeof(LTNode)); if (*phead != NULL) {(*phead)->next = *phead;(*phead)->prev = *phead; }
}

phead 代表指向链表的“头节点”的指针

在这个初始化函数中,新创建的链表头节点的 next 和 prev 指针都被设置为指向自身,形成一个空的双向循环链表,这里用了二级指针,是因为我们对phead进行了改变,对指针进行改变,则需要二级指针
这种方法我们初始化格式如下,首先创造一个plist结构体指针,再传参

LTNode* plist;
LTInit(&plist);
LTNode* LTInit2() {LTNode* phead = (LTNode*)malloc(sizeof(LTNode));if (phead != NULL) {phead->prev = phead;phead->next = phead;}return phead; 
}

在这个实现中,LTInit函数不接受任何参数,而是直接创建并初始化一个新的头节点,使其prev和next指针都指向自己,从而形成一个空的双向循环链表。这样设计的好处是简化了链表的初始化过程,你只需要调用LTInit来获取一个新的链表头节点即可
这种方法我们直接用plist接收返回值即可

LTNode* plist=LITnit2();

销毁双向链表

void ListDestroy(LTNode* phead) {if (phead == NULL) {return;}// 由于是循环链表,我们需要一个指针指向第一个节点LTNode* current = phead->next;// 如果链表不只是头节点自己循环(即有实际数据节点)if (current != phead) {do {LTNode* temp = current;current = current->next; // 移动到下一个节点free(temp); // 释放当前节点内存} while (current != phead);}// 最后,释放头节点内存(如果头节点是哨兵节点并且是动态分配的)free(phead);
}

函数首先检查传入的链表是否为空。如果不为空,它会进入一个 do-while 循环,这个循环确保至少运行一次,即使链表中只有一个节点(头节点)
在循环内部,它会释放当前节点的内存,并移动到下一个节点,直到它循环回到头节点。最后,它释放头节点的内存

链表的打印

在单链表中,我们进行循环打印的判断条件是最后一个节点的指针是否指向NULL,而在双向循环链表中,没有空指针,我们的判断条件也有所不同

void LTPrint(LTNode* phead) {if (phead == NULL || phead->next == phead) {return;}LTNode* current = phead->next;while (current != phead) { printf("%d ", current->val); current = current->next; }printf("\n"); 
}

首先

if (phead == NULL || phead->next == phead) {return;
}

这串代码是判断链表是否为空或者链表是否只有一个头结点,如果是,则没有数据可打印,直接返回

遍历链表:

LTNode* current = phead->next;
while (current != phead) { printf("%d ", current->val); current = current->next; 
}

这部分代码初始化一个新指针 current 指向链表的第一个节点(即 phead->next),然后进入一个 while 循环。在循环中,只要 current 不指回 phead,它就打印当前节点的值,并移动到下一个节点。这个循环确保了所有节点都被访问一次。

注意,由于它从 phead->next 开始,phead 本身不存储有效数据(或者说是一个哨兵节点)

双向链表头尾的插与删

尾插

void LTPushBack(LTNode* phead, LTDatatype x) {LTNode* newnode = CreatNode(x);if (phead == NULL) {return;}newnode->next = phead; newnode->prev = phead->prev;phead->prev->next = newnode;phead->prev = newnode;
}

在这里插入图片描述
我们构建newnode

  • newnode的next指向头结点newnode->next = phead;
  • 原来的phead的prev指针指向倒数第二个节点,那么newnode的前一个指针则为初始时phead的prev指针newnode->prev = phead->prev;
  • 现在更新倒数第二个节点的下一个指针,原来指向头指针,现在指向newnode:phead->prev->next = newnode;
  • 最后更改phead的prev指针,指向尾部的newnodephead->prev = newnode;

测试代码如下:
在这里插入图片描述

尾删

void LTPopBack(LTNode* phead) {if (phead == NULL || phead->next == phead) {return;}LTNode* tail = phead->prev; LTNode* tailprev = tail->prev; // 断开当前末尾节点与链表的连接,形成新的末尾tailprev->next = phead;phead->prev = tailprev;// 释放原末尾节点占用的内存free(tail);
}
  • 首先判断是否为空链表或者只有哨兵节点,如果是则没有值可以删除,直接返回
  • 找到尾部节点tail,即头结点的前一个指针指向的节点;
  • 再找到tail前面的节点,即预期的尾节点将这个节点的下一个指针指向头结点,并将头节点的前一个指针指向这个节点
  • 将tail这个尾部节点内存释放

测试代码如下:
在这里插入图片描述

头插

void LTPushFront(LTNode* phead, LTDatatype x) {LTNode* newnode = CreatNode(x); if (phead == NULL) {return;}newnode->next = phead->next; newnode->prev = phead;       phead->next->prev = newnode; phead->next = newnode;       
}
  • 首先判断链表是否为空,为空直接返回
  • 新节点的next指针指向原来头节点的下一个节点:newnode->next = phead->next;
  • 新节点的prev指针指向头结点:newnode->prev = phead;
  • 接着更新头节点之后的节点的prev指针,以及头节点的next指针
    - 原来头节点之后的节点的prev指针现在应该指向新节点:phead->next->prev = newnode;
    - 头节点的next指针现在应该指向新节点:phead->next = newnode;

我们更新了四个指针:新节点的前后指针,头结点的next指针,后一个节点的prev指针

测试代码:
在这里插入图片描述

头删

void LTPopFront(LTNode* phead) {if (phead == NULL || phead->next == phead) {return;}LTNode* first = phead->next;phead->next = first->next;first->next->prev = phead;free(first);
}
  • 首先检查链表是否为空或者只有哨兵节点
  • 找到要删除的节点,它是头节点的下一个节点:LTNode* first = phead->next;
  • 更新头节点的next指向被删除节点的下一个节点:phead->next = first->next;
  • 更新新的第一个有效数据节点的prev指向头节点:first->next->prev = phead;
  • 最后释放被删除节点所占用的内存

测试代码:
在这里插入图片描述

查找特定节点

LTNode* ListFind(LTNode* phead, int x) {if (phead == NULL || phead->next == phead) {return NULL;}LTNode* current = phead->next; while (current != phead) { if (current->val == x) {return current;}current = current->next; }return NULL;
}
  • 如果链表为空或者只有哨兵节点,直接返回
  • 由于第一个节点没有有效数据,我们可以从 phead 的下一个节点开始遍历
  • 在这个实现中,我们从哨兵节点的下一个节点开始遍历,即从链表的第一个实际数据节点开始。循环继续执行,直到 current 指针再次回到哨兵节点 phead。如果找到一个节点的值与 x 相等,函数返回该节点的指针。如果遍历完所有节点都没有找到,则返回 NULL。

在指定位置前插入数据

void ListInsert(LTNode* pos, LTDatatype x)
{if (pos == NULL){return;}LTNode* posprev = pos->prev;LTNode* newnode = CreatNode(x);posprev->next = newnode;newnode->prev = posprev;newnode->next = pos;pos->prev = newnode;
}
  1. 找到pos前面的节点posprev
  2. 构建新节点
  3. posprev的next指针指向newnode;
  4. newnode的prev指针指向posprev,next指针指向pos
  5. pos的前一个指针指向newnode;

测试代码,在1 2 3 4 5的3前面插入8,首先获得3节点的地址,在传入插入函数中

在这里插入图片描述
如果再哨兵节点位置,往前插入,则相当于尾插

删除pos节点

我们假设pos不为哨兵节点

void ListErase(LTNode* pos) {if (pos == NULL) {return;}pos->prev->next = pos->next;pos->next->prev = pos->prev;free(pos);
}

这个代码就非常简单了,改变指针后将空间释放

测试代码,删除1 2 3 4 5中的3
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/b7333188533e4900a9d0d59b6234dd66.png
这里注意置空temp

总结

对比于顺序表,双向带头循环链表有以下优势:

  • 在任意位置添加或删除元素的时间复杂度都是O(1)
  • 按需要进行申请空间,没有浪费

不足之处

  • 下标随机访问不方便,需要遍历链表,时间复杂度为O(N);

顺序表和双向带头链表根据特定的使用场景和需求具有各自的优势和劣势。选择哪种数据结构,取决于对性能、内存使用、以及操作灵活性的具体要求。

本节内容到此结束,感谢大家的阅读!!!

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

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

相关文章

Medical Boundary Diffusion Modelfor Skin Lesion Segmentation

皮肤病灶分割的医学边界扩散模型 摘要 由于多尺度边界关注和特征增强模块的进步,皮肤镜图像中的皮肤病变分割最近取得了成功。然而,现有的方法依赖于端到端学习范式,直接输入图像和输出分割图,经常与极其困难的边界作斗争&#…

B端系统升级方案模板:针对美观性和体验性升级(总体方案)

大家好,我是大美B端工场,专注于前端开发和UI设计,有需求可以私信。本篇从全局分享如何升级B端系统,搞B端系统升级的有个整体思维,不是说美化几个图标,修改几个页面就能解决的,这个方案模板&…

Spring AMQP(3.1.1)设置ConfirmCallback和ReturnsCallback

文章目录 一、起因二、代码1. 定义exchange和queue2. RabbitTemplate3. EnhancedCorrelationData4. 发送消息 环境如下 VersionSpringBoot3.2.1spring-amqp3.1.1RabbitMq3-management 一、起因 老版本的spring-amqp在CorrelationData上设置ConfirmCallback。但是今天却突然发…

非常详细!操作系统【IO核心子系统】

🌈个人主页:godspeed_lucip 🔥 系列专栏:OS从基础到进阶 🏆🏆本文完整PDF源文件请翻阅至文章底部下载。🏆🏆 1 I/O核心子系统——概述1.1 核心子系统要完成的功能1.1.1 假脱机技术1…

Python 异常处理及程序调试

Python 是一门功能强大而又易于学习的编程语言,它提供了丰富的工具和库来帮助开发者编写高效、稳定的程序。然而,在编写复杂的应用程序时,错误和异常是难以避免的。本文将介绍 Python 中的异常处理机制以及程序调试技巧,帮助读者提…

JavaScript中什么是事件委托

JavaScript 中的事件委托(Event delegation)是一种重要的编程技术,它能够优化网页中的事件处理,提高程序的性能和可维护性。本文将详细介绍事件委托的概念、工作原理,并提供示例代码来说明其实际应用。 事件委托是基于…

Linux版Black Basta勒索病毒针对VMware ESXi服务器

前言 Black Basta勒索病毒是一款2022年新型的勒索病毒,最早于2022年4月被首次曝光,主要针对Windows系统进行攻击,虽然这款新型的勒索病毒黑客组织仅仅才出来短短两个多月的时间,就已经在其暗网平台上已经公布了几十个受害者之多&…

【案例8】用户中心实现涉及内容和过程

图1 如图1是用盒子模型内容实现的,但是需要了解一些内容。 一.内容知识引入 1.内边距属性(padding) 为了调整盒子在网页中的显示位置,常常需要为元素设置内边距。内边距也被称为内填充,是指元素内容和边框之间的距离…

linux安装mysql8且初始化表名忽略大小写

mysql8下载地址 MySQL8.0安装步骤 1、把安装包上传到linux系统,解压、重命名并移动到/usr/local/目录: cd ~ tar -xvf mysql-8.0.32-linux-glibc2.12-x86_64.tar.xz mv mysql-8.0.32-linux-glibc2.12-x86_64/ mysql80/ mv mysql80/ /usr/local/2、在M…

VMwareWorkstation17.0虚拟机安装Windows2.03完整详细步骤图文教程

VMwareWorkstation17.0虚拟机安装Windows2.03完整详细步骤图文教程 第一篇 下载Windows2.03第二篇 配置Windows2.03虚拟机机器环境第三篇 启动Windows2.03系统 第一篇 下载Windows2.03 1.Windows2.0原版软盘下载地址是 暂不提供,后续更新 2.Windows2.03虚拟机镜像下…

鸿蒙开发-HarmonyOS UI架构

初步布局Index 当我们新建一个工程之后,首先会进入Index页。我们先简单的做一个文章列表的显示 class Article {title?: stringdesc?: stringlink?: string }Entry Component struct Index {State articles: Article[] []build() {Row() {Scroll() {Column() …

GiantPandaCV | 视觉类表面缺陷检测项目相关技术总结

本文来源公众号“GiantPandaCV”,仅用于学术分享,侵权删,干货满满。 原文链接:视觉类表面缺陷检测项目相关技术总结 本文由海滨撰写,首发于GaintPandaCV。 零、前言 做这个方向的项目也有一段时间了,作为…

Deep learning学习笔记

lec 1:Regression 1.5 Linear neural networks for regression线性神经网络的回归 I parameterizing output layer, I handling data, I specifying loss function, I training model. 浅层网络包括线性模型,其中包含了许多经典的统计预测方法&…

C++中的拷贝构造函数

一、拷贝构造函数的概念 拷贝构造函数用于创建一个与已有对象相同的对象,本质上也是构造函数的重载 拷贝构造函数只有一个类型为 const 类类型引用的形参,当我们要创建一个与已存在对象相同的对象时,由编译器自动调用拷贝构造函数。 clas…

简单的edge浏览器插件开发记录

今天在浏览某些网页的时候,我想要屏蔽掉某些信息或者修改网页中的文本的颜色、背景等等。于是在浏览器的控制台中直接输入JavaScript操作dom完成了我想要的功能。但是每次在网页之间跳转该功能都会消失,我需要反复复制粘贴js脚本,无法实现自动…

MATLAB知识点:exprnd函数(★★☆☆☆)生成指数分布的随机数

讲解视频:可以在bilibili搜索《MATLAB教程新手入门篇——数学建模清风主讲》。​ MATLAB教程新手入门篇(数学建模清风主讲,适合零基础同学观看)_哔哩哔哩_bilibili 节选自第3章:课后习题讲解中拓展的函数 在讲解第三…

【革新你的社交形象】用AI创意头像应用,让你的头像独一无二!

在这个数字化时代,社交媒体已经成为我们生活中不可或缺的一部分。你是否曾经为了找到一个既能表达自己个性,又足够吸引眼球的头像而苦恼?现在,有了我们全新推出的AI创意头像应用,你的这一困扰将成为过去! …

React入门到精通:掌握前端开发的必备技能!

介绍:React是一个由Facebook开发和维护的JavaScript库,用于构建用户界面,特别是用于构建单页应用程序和移动应用程序的用户界面。以下是对React的详细介绍: 虚拟DOM:React通过使用虚拟DOM(Document Object …

【Deep Learning 2】神经网络的优化

🌞欢迎来到PyTorch的世界 🌈博客主页:卿云阁 💌欢迎关注🎉点赞👍收藏⭐️留言📝 🌟本文由卿云阁原创! 📆首发时间:🌹2024年2月16日&a…

java8-重构、测试、调试

8.1.1 改善代码的可读性 改善代码的可读性到底意味着什么?我们很难定义什么是好的可读性,因为这可能非常主观。通常的理解是,“别人理解这段代码的难易程度”。改善可读性意味着你要确保你的代码能非常容易地被包括自己在内的所有人理解和维护。为了确保…