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

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

双向链表

  • 双向链表、头节点和循环的介绍
  • 构建双向链表
    • 节点的构建
    • 初始化双向循环链表(空链表)
    • 销毁双向链表
  • 链表的打印
  • 双向链表头尾的插与删
    • 尾插
    • 尾删
    • 头插
    • 头删
  • 查找特定节点
    • 在指定位置前插入数据
    • 删除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.rhkb.cn/news/258417.html

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

相关文章

009集——磁盘详解——电脑数据如何存储在磁盘

很多人也知道数据能够保存是由于设备中有一个叫做「硬盘」的组件存在,但也有很多人不知道硬盘是怎样储存这些数据的。这里给大家讲讲其中的原理。 首先我们要明白的是,计算机中只有0和1,那么我们存入硬盘的数据,实际上也就是一堆0…

Linux常见指令(一)

一、基本指令 1.1ls指令 语法 : ls [ 选项 ][ 目录或文件 ] 功能:对于目录,该命令列出该目录下的所有子目录与文件。对于文件,将列出文件名以及其他信息。 常用选项: -a 列出目录下的所有文件,包括以 .…

【Java程序员面试专栏 分布式中间件】Redis 核心面试指引

关于Redis部分的核心知识进行一网打尽,包括Redis的基本概念,基本架构,工作流程,存储机制等,通过一篇文章串联面试重点,并且帮助加强日常基础知识的理解,全局思维导图如下所示 基础概念 明确redis的特性、应用场景和数据结构 什么是Redis,Redis有哪些应用场景 Redi…

CSP-动态规划-最长公共子序列(LCS)

一、动态规划 动态规划(Dynamic Programming,简称DP)主要用于求解可以被分解为相似子问题的复杂问题,特别是在优化问题上表现出色,如最短路径、最大子数组和、编辑距离等。动态规划的核心思想是将原问题分解为较小的子…

Python第十七章(继承)

继承:子类继承父类的所有方法和属性 一。单继承:一个子类继承一个父类 注释:B是子类,继承了A的函数方法,当调用B时候,会同时使用A中的全部方法,object类是顶级类或者基类,其他子类叫…

机器学习入门--门控循环单元(GRU)原理与实践

GRU模型 随着深度学习领域的快速发展,循环神经网络(RNN)已成为自然语言处理(NLP)等领域中常用的模型之一。但是,在RNN中,如果时间步数较大,会导致梯度消失或爆炸的问题,…

《山雨欲来-知道创宇 2023 年度 APT 威胁分析总结报告》

下载链接: https://pan.baidu.com/s/1eaIOyTk12d9mcuqDGzMYYQ?pwdzdcy 提取码: zdcy

【sgCreateTableColumn】自定义小工具:敏捷开发→自动化生成表格列html代码(表格列生成工具)[基于el-table-column]

源码 <template><!-- 前往https://blog.csdn.net/qq_37860634/article/details/136126479 查看使用说明 --><div :class"$options.name"><div class"sg-head">表格列生成工具</div><div class"sg-container"…

python in Vscode

背景 对于后端的语言选择&#xff1a; python&#xff0c;java&#xff0c;JavaScript备选。 选择Python 原因&#xff1a;可能是非IT专业的人中&#xff0c;会Python的人比较多。 目的 之前使用的IDE是VSCODE&#xff0c;在WSL的环境下使用。现在需要在在WSL的VSCODE下使…

使用Properties类读取配置文件

读取配置文件 使用Properties类读取配置文件。 Properties类本质上是个hashmap 常用方法 getProperty ( String key)&#xff1a; 用指定的键在此属性列表中搜索属性。也就是通过参数 key &#xff0c;得到 key 所对应的 value。load ( InputStream inStream)&#xff1a; 从输…

字符串拼接 - 华为OD统一考试(C卷)

OD统一考试&#xff08;C卷&#xff09; 分值&#xff1a; 200分 题解&#xff1a; Java / Python / C 题目描述 给定 M 个字符( a-z ) &#xff0c;从中取出任意字符(每个字符只能用一次)拼接成长度为 N 的字符串&#xff0c;要求相同的字符不能相邻。 计算出给定的字符列表…

Maui blazor ios 按设备类型设置是否启用safeArea

需求&#xff0c;新做了个app&#xff0c; 使用的是maui blazor技术&#xff0c;里面用了渐变背景&#xff0c;在默认启用SafeArea情况下&#xff0c;底部背景很突兀 由于现版本maui在SafeArea有点bug&#xff0c;官方教程的<ContentPage SafeAreafalse不生效&#xff0c;于…

【机器学习】数据清洗之识别重复点

&#x1f388;个人主页&#xff1a;甜美的江 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;机器学习 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进步…

STM32 HAL库 STM32CubeMX -- IWDG(独立看门狗)

STM32 HAL库 STM32CubeMX -- IWDG 一、IWDG简介二、独立看门狗的工作原理三、驱动函数初始化函数HAL IWDG Init()初始化函数HAL IWDG Init()其他宏函数 四、超时时间计算第一种办法第二种办法&#xff08;推荐&#xff09; 一、IWDG简介 看门狗(Watchdog)就是MCU上的一种特殊的…

SORA:OpenAI最新文本驱动视频生成大模型技术报告解读

Video generation models as world simulators&#xff1a;作为世界模拟器的视频生成模型 1、概览2、Turning visual data into patches&#xff1a;将视觉数据转换为补丁3、Video compression network&#xff1a;视频压缩网络4、Spacetime Latent Patches&#xff1a;时空潜在…

SAP PP学习笔记- 豆知识02 - 品目要谁来维护?怎么决定更不更新品目的数量金额?

其实都是在品目类型的Customize中设定的。 咱们这里简单试着说一下什么场景使用。 1&#xff0c;SAP中品目有很多View&#xff0c;都要由哪些部门来维护呢&#xff1f; 其实就是谁用谁维护呗。 在新建一个品目的时候&#xff0c;品目Type本身就决定了该品目要由哪些部门来维…

【STM32 CubeMX】串口编程DMA

文章目录 前言一、DMA方式1.1 DMA是什么1.2 CubeMX配置DMA1.3 DMA方式函数使用DMA的发送接收函数 总结 前言 在嵌入式系统中&#xff0c;串口通信是一项至关重要的功能&#xff0c;它允许单片机与外部设备进行数据交换&#xff0c;如传感器、显示器或其他设备。然而&#xff0…

【数据结构】16 二叉树的定义,性质,存储结构(以及先序、后序、中序遍历)

二叉树 一个二叉树是一个有穷的结点集合。 它是由根节点和称为其左子树和右子树的两个不相交的二叉树组成的。 二叉树可具有以下5种形态。 性质 一个二叉树第i层的最大结点数为 2 i − 1 2^{i-1} 2i−1, i ≥ 1 i \geq 1 i≥1 每层最大结点可以对应完美二叉树&#xff08;…

阿里云服务器租用收费标准价格表(2024年更新)

2024年最新阿里云服务器租用费用优惠价格表&#xff0c;轻量2核2G3M带宽轻量服务器一年61元&#xff0c;折合5元1个月&#xff0c;新老用户同享99元一年服务器&#xff0c;2核4G5M服务器ECS优惠价199元一年&#xff0c;2核4G4M轻量服务器165元一年&#xff0c;2核4G服务器30元3…

Gitee入门之工具的安装

一、gitee是什么&#xff1f; Gitee&#xff08;码云&#xff09;是由开源中国社区在2013年推出的一个基于Git的代码托管平台&#xff0c;它提供中国本土化的代码托管服务。它旨在为个人、团队和企业提供稳定、高效、安全的云端软件开发协作平台&#xff0c;具备代码质量分析、…