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

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

双向链表

  • 双向链表、头节点和循环的介绍
  • 构建双向链表
    • 节点的构建
    • 初始化双向循环链表(空链表)
    • 销毁双向链表
  • 链表的打印
  • 双向链表头尾的插与删
    • 尾插
    • 尾删
    • 头插
    • 头删
  • 查找特定节点
    • 在指定位置前插入数据
    • 删除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端系统升级的有个整体思维,不是说美化几个图标,修改几个页面就能解决的,这个方案模板&…

laravel_进程门面_再次介绍

文章目录 单次调用进程池调用伪造和断言断言和判断的区别 前面我已经简单介绍了一点process facades,这里再进行一个补充的介绍。 可以也看看前面的介绍。 单次调用 use Illuminate\Support\Facades\Process;$result Process::run(ls -la);return $result->ou…

c++希尔排序解释

希尔排序是一种排序算法,它是插入排序的改进版本。它通过将序列分成多个子序列,对每个子序列进行插入排序,然后逐步减小子序列的长度,直到整个序列有序。 希尔排序的基本思想是:选择一个增量序列,根据增量…

【天枢系列 01】Linux行数统计:命令对决,谁才是王者?

文章目录 01 统计行数命令1.1 wc 命令1.2 grep 命令1.3 sed 命令1.4 awk 命令1.5 Perl 命令 02 Linux的wc命令详细用法2.1 基本语法2.2 主要选项2.3 附加选项2.4 示例用法2.5 注意事项【重要!】 03 Linux的grep命令详细用法3.1 基本语法3.2 主要选项3.3 示例用法3.4…

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…

Legba:一款功能强大的多协议凭证爆破与密码喷射枚举工具

关于Legba Legba是一款功能强大的多协议凭证爆破与密码喷射枚举工具。该工具的目的就是在消耗更少资源的同时实现更好的性能和稳定性,可以帮助广大研究人员执行多种协议场景下的凭证爆破,以及密码喷射和枚举任务。 支持的协议和功能 AMQP(…

Python 异常处理及程序调试

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

JavaScript中什么是事件委托

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

PointMixer论文阅读笔记

MLP-mixer是最近很流行的一种网络结构,比起Transformer和CNN的节构笨重,MLP-mixer不仅节构简单,而且在图像识别方面表现优异。但是MLP-mixer在点云识别方面表现欠佳,PointMixer就是在保留了MLP-mixer优点的同时,还可以…

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

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

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

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

用keras对电影评论进行情感分析

文章目录 下载IMDb数据读取IMDb数据建立分词器将评论数据转化为数字列表让转换后的数字长度相同加入嵌入层建立多层感知机模型加入平坦层加入隐藏层加入输出层查看模型摘要训练模型评估模型准确率进行预测查看测试数据预测结果完整函数用RNN模型进行IMDb情感分析用LSTM模型进行…

【水文】判断同构数

正整数n若是它平方数的尾部&#xff0c;则称n为同构数。比如6是其平方数36的尾部&#xff0c;76是其平方 数5776的尾部&#xff0c;6与76都是同构数。输入正整数&#xff0c;判断是否为同构数。如果是&#xff0c;那么输出 n1;如果不是&#xff0c;那么输出n0。 #include <…

如何在Python代码中使用注解

在Python代码中使用注解&#xff08;Annotations&#xff09;主要涉及对函数参数和返回值进行类型标注。这些注解在运行时并不强制执行&#xff0c;但可以用于类型检查、提高代码可读性以及为IDE和其他工具提供更好的代码补全和错误检查功能。 以下是如何在Python代码中使用注…

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

mysql8下载地址 MySQL8.0安装步骤 1、把安装包上传到linux系统&#xff0c;解压、重命名并移动到/usr/local/目录&#xff1a; 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…

TCP/IP C 语言实现单个客户端和服务端 TCP 通信

这是多线程服务端 #include <stdio.h> #include <winsock2.h> #include <pthread.h> #include <string.h> #include <conio.h> #pragma comment(lib,"ws2_32.lib") #pragma warning(disable : 4996) // 客户端结构体 typedef struct…

Springboot-接入WebSocket服务

1、依赖引入 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId> </dependency> 2、启动类添加bean public class Application {/*** 会自动注册使用了ServerEndpoint注…

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

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