当前位置: 首页 > news >正文

C语言数据结构——详细讲解 双链表

从单链表到双链表:数据结构的演进与优化

  • 前言
  • 一、单链表回顾
  • 二、单链表的局限性
  • 三、什么是双链表
  • 四、双链表的优势
    • 1.双向遍历
    • 2.不带头双链表的用途
    • 3.带头双链表的用途
  • 五、双链表的操作
    • 双链表的插入操作
      • (一)双链表的尾插操作
      • (二)双链表的头插操作
      • (三)双链表在指定位置之后插入数据
    • 双链表的删除操作
      • (一)双链表的尾删操作
      • (二)双链表的头删操作
      • (三)双链表删除指定位置数据


前言

         在数据结构的广袤天地里,链表作为一种重要的线性数据结构,以其独特的存储方式和灵活的操作特性,在众多编程场景中发挥着关键作用。今天,我们就来深入探讨一下从单链表双链表的发展历程,看看双链表是如何在单链表的基础上应运而生,并解决了单链表所面临的一些问题


一、单链表回顾

  • 首先,我们来回顾一下单链表的基本结构。单链表是由一系列节点组成的数据结构,每个节点包含两部分数据域指针域数据域用于存储具体的数据指针域则存储指向下一个节点的指针,通过这样的指针链接,各个节点依次相连,形成了一条链表。
  • 单链表不带头单向不循环,是链表结构中较为基础的一种形式,与带头节点的单链表不同不带头节点的单链表在进行操作时,第一个节点就直接存储了实际的数据,没有专门设置一个额外的头节点来简化某些操作逻辑。

以下是带头单链表和不带头单链表的图片

在这里插入图片描述
在这里插入图片描述

以上这些图片均表示单链表

  • 例如,我们有一个简单的单链表存储整数数据,节点结构可以定义如下
typedef struct ListNode 
{int data;struct ListNode *next;
} ListNode;
  • 单链表很多场景下都非常有用,它能够方便地进行动态内存分配实现数据的灵活存储操作,比如插入删除节点等操作相对数组来说更加灵活,不需要移动大量元素

二、单链表的局限性

  • 然而,单链表也存在一些局限性:
  • 单向遍历

单链表的指针只能指向一个方向,也就是只能从表头顺着指针依次访问到表尾的节点

  • 删除操作的不便:

当我们想要删除单链表中的某个节点时,如果只知道要删除节点的指针(而不是它的前驱节点指针),操作起来会比较麻烦,,往往需要从头开始遍历链表去找到前驱节点,这同样会影响操作的效率。

为了克服单链表的这些局限性,我们就引入了双链表

在这里插入图片描述

三、什么是双链表

  • 双链表的每个节点除了包含数据域和指向下一个节点的指针域外还增加了一个指向前一个节点的指针域。也就是说,双链表的节点结构可以定义如下:

在这里插入图片描述

typedef struct DoublyListNode 
{int data;struct DoublyListNode *prev;struct DoublyListNode *next;
} DoublyListNode;
  • 在这个结构中,**prev 指针指向当前节点的前一个节点,next 指针指向当前节点的下一个节点。**这样一来,双链表就具备了双向遍历的能力。

双链表可以分为带头节点和不带头结点

  • 不带头节点的双链表
    在这里插入图片描述
  • 结构特点:

1.链表的第一个节点就直接存储实际的数据元素,不存在一个专门作为 “头” 的额外节点。
2.每个节点除了数据域外,有两个指针域,一个指向前驱节点(prev 指针),一个指向后继节点(next 指针)

  • 带头结点的双链表
    在这里插入图片描述

  • 结构特点:

1.有一个专门的头节点,这个头节点的数据域一般不存储实际的数据(当然也可以根据具体需求赋予特殊含义,但通常为空数据域),主要作用是方便链表的各种操作。
2.头节点同样有两个指针域,prev 指针一般指向 NULL(因为它是表头,前面没有节点了),next 指针指向链表中的第一个实际存储数据的节点。

四、双链表的优势

1.双向遍历

从表头到表尾遍历:和单链表类似,我们可以通过每个节点的 next 指针依次访问后续节点,从而实现从表头到表尾的遍历。

  • 删除操作的便利性

在双链表中删除一个节点就变得更加容易了。假设我们要删除节点 p,我们可以直接通过 p 的 prev 和 next 指针来完成删除操作,而不需要像单链表那样去专门寻找它的前驱节点

总结一下带头和不带头有什么用

2.不带头双链表的用途

  • 节省内存空间:

每一个节点都实实在在地用于存储数据及相关指针,不存在一个仅为了方便操作而占据空间但通常不存储实际有效数据的头节点。

  • 体现数据原始性

3.带头双链表的用途

  • 简化操作逻辑:
  • 插入 无论是头插、尾插还是指定位置插入,由于有头节点的存在,不需要针对链表为空的情况单独设计特殊的处理逻辑。

  • 删除 进行头删、尾删或指定位置删除时,操作逻辑相对统一,不用特别考虑链表为空时删除操作的特殊变化。

  • 遍历 遍历链表时,从头节点的下一个节点开始顺着 next 指针向后遍历(正向遍历)以及从链表末尾顺着 prev 指针向前遍历(反向遍历)的逻辑较为清晰、简单,不用像不带头双链表那样,正向遍历要从第一个实际存储数据的节点开始,反向遍历要先找到末尾节点。

五、双链表的操作

双链表的插入操作

(一)双链表的尾插操作

void LTTPushFront(LTNode* head, int x)
{assert(head);LTNode* newnode = new LTNode;newnode->data = x;newnode->next = head;head = newnode;// 打印链表printList(head);
}
  • head 为双链表的头结点
  • x为要插入的数值

插入节点的步骤

  • LTNode* newnode = new LTNode创建新节点
  • newnode->data = x初始化新节点的数据
  • newnode->next = head
  • 这行代码将新节点的next指针指向当前的头节点head。这样,新节点就指向了原来的第一个节点
  • head = newnode
  • 这行代码将头节点head更新为新节点newnode。这样,新节点就成为了链表的新头节点。

(二)双链表的头插操作

// 头插操作
void LTPushFront(LTNode* head, int x) 
{assert(head);LTNode* newnode = buyNode(x);newnode->next = head->next;newnode->prev = head;head->next->prev = newnode;head->next = newnode;
}
  • newnode->next = head->next;
  • newnode->prev = head;
  • 第一行代码将新节点newnode的next指针指向当前头节点head的下一个节点(即原来链表中的第一个节点)。
  • 第二行代码将新节点newnode的prev指针指向头节点head
  • head->next->prev = newnode;
  • 将原来链表中第一个节点(即head->next)的prev指针指向新节点newnode。这一步是为了让原来的第一个节点知道它的前一个节点变成了新节点
  • head->next = newnode;
  • 将头节点head的next指针指向新节点newnode。这一步是为了让头节点指向新插入的节点,从而使新节点成为链表中的第一个节点

(三)双链表在指定位置之后插入数据

// 在pos位置之后插入数据
void LTInsert(LTNode* pos, int x) {assert(pos);LTNode* newnode = buyNode(x);newnode->next = pos->next;newnode->prev = pos;if (pos->next) {pos->next->prev = newnode;}pos->next = newnode;
}
  • newnode->next = pos->next;:
  • 将新节点newnode的next指针指向pos节点的下一个节点。
  • newnode->prev = pos;
  • 将新节点newnode的prev指针指向pos节点
  • if (pos->next) { pos->next->prev = newnode; }:
  • 如果pos节点有下一个节点(即pos->next不为NULL),那么将pos的下一个节点的prev指针指向新节点newnode。
  • 这一步是为了保持双链表的双向连接关系
  • 最后将pos节点的next指针指向新节点newnode,完成新节点在pos节点之后的插入操作

双链表的删除操作

(一)双链表的尾删操作

// 尾删
void LTPopBack(LTNode* head) 
{assert(!LTEmpty(head));LTNode* del = head->prev;LTNode* prev = del->prev;prev->next = head;head->prev = prev;delete del;
}
  • LTNode* del = head->prev;
  • 双链表通常有一个头节点,尾节点是头节点的前一个节点,所以这里head->prev就是尾节点,将其赋值给del
  • LTNode* prev = del->prev;,找到尾节点del的前一个节点prev
  • prev->next = head;,将尾节点的前一个节点的next指针指向头节点,这样就把尾节点从链表中移除了。
  • head->prev = prev;,同时更新头节点的prev指针指向新的尾节点(原来尾节点的前一个节点)

(二)双链表的头删操作

// 头删操作
void LTpopFront(LTNode* head) {assert(!LTEmpty(head));LTNode* del = head->next;head->next = del->next;if (del->next) {del->next->prev = head;}delete del;
}
  • LTNode* del = head->next;头节点的下一个节点就是要删除的第一个数据节点,将其赋值给del
  • head->next = del->next;,将头节点的next指针指向要删除节点del的下一个节点,这样就把del从链表中移除了。
  • if (del->next) { del->next->prev = head; },如果del有下一个节点(即del不是尾节点),那么将del的下一个节点的prev指针指向头节点

(三)双链表删除指定位置数据

// 删除指定位置数据
void LTEarse(LTNode* pos) {assert(pos);pos->prev->next = pos->next;if (pos->next) {pos->next->prev = pos->prev;}delete pos;
}
  • pos->prev->next = pos->next;,将pos节点的前一个节点的next指针指向pos节点的下一个节点,这样就把pos从链表中移除了。
  • if (pos->next) { pos->next->prev = pos->prev; },如果pos有下一个节点,那么将pos的下一个节点的prev指针指向pos的前一个节点
文章到这里就结束了,感谢你的阅读,喜欢的话记得三连

在这里插入图片描述

相关文章:

C语言数据结构——详细讲解 双链表

从单链表到双链表:数据结构的演进与优化 前言一、单链表回顾二、单链表的局限性三、什么是双链表四、双链表的优势1.双向遍历2.不带头双链表的用途3.带头双链表的用途 五、双链表的操作双链表的插入操作(一)双链表的尾插操作(二&a…...

Shell脚本基础(4):条件判断

内容预览 ≧∀≦ゞ Shell脚本基础(4):条件判断声明导语基本的if语句结构数值比较运算符文件测试运算符扩展:使用elif和else使用&&和||结合条件判断小结 Shell脚本基础(4):条件判断 声明…...

在 Swift 中实现字符串分割问题:以字典中的单词构造句子

文章目录 前言摘要描述题解答案题解代码题解代码分析示例测试及结果时间复杂度空间复杂度总结 前言 本题由于没有合适答案为以往遗留问题,最近有时间将以往遗留问题一一完善。 LeetCode - #140 单词拆分 II 不积跬步,无以至千里;不积小流&…...

win10中使用ffmpeg和MediaMTX 推流rtsp视频

在win10上测试下ffmpeg推流rtsp视频,需要同时用到流媒体服务器MediaMTX 。ffmpeg推流到流媒体服务器MediaMTX ,其他客户端从流媒体服务器拉流。 步骤如下: 1 下载MediaMTX github: Release v1.9.3 bluenviron/mediamtx GitHub​​​​​…...

16. 【.NET 8 实战--孢子记账--从单体到微服务】--汇率获取定时器

这篇文章我们将一起编写这个系列专栏中第一个和外部系统交互的功能:获取每日汇率。下面我们一起来编写代码吧。 一、需求 根据文章标题可知,在这片文章中我们只进行汇率的获取和写入数据库。 编号需求说明1获取每日汇率1. 从第三方汇率API中获取汇率信…...

C#元组详解:创建、访问与解构

在C#中,元组(Tuple)是一种数据结构,用于将多个元素组合成一个单一的对象。元组可以包含不同类型的元素,并且每个元素都有一个指定的位置(索引)。元组在需要临时组合多个值而不想创建自定义类时非…...

wsl2安装

Windows Subsystem for Linux 2 (WSL2) 是 Windows 10 和 Windows 11 中用于运行 Linux 二进制可执行文件的兼容层。WSL2 是 WSL 的最新版本,提供了更快的文件系统性能和完整的系统调用兼容性。本教程将指导你如何在 Windows 系统上安装 WSL2。 前提条件 操作系统要…...

android studio无法下载,Could not GET xxx, Received status code 400

-- 1. 使用下面的地址代替 原地址: distributionUrlhttps\://services.gradle.org/distributions/gradle-6.5-all.zip 镜像地址: distributionUrlhttps\://downloads.gradle-dn.com/distributions/gradle-6.5-all.zips 上面的已经不好用了 https\://mirrors.cloud.tencent.c…...

RUST学习教程-安装教程

文章目录 参考文档安装教程更新卸载 参考文档 https://course.rs/first-try/installation.html 安装教程 Linux或者mac安装教程 curl --proto https --tlsv1.2 https://sh.rustup.rs -sSf | sh安装完成,当出现command not found的时候,需要source一下…...

redis6.0之后的多线程版本的问题

一、redis早期版本和新版本的讨论 这个问题其实有些废话,哪个版本肯定都有不同啊。其实这里主要是提到的网上的大家对Redis6.0中的多线程版本的不同即以前宣传的Redis是单线程程版的,之后变成了多线程版本的。网上对这个讨论非常激烈,反正各…...

python的 pandas.Dataframe 和 pandas.Series基础内容

目录 0 有一个比较麻烦琐碎的地方 1 python pandas.Dataframe 2 pd.concat() 可以合并 pd.Dataframe 2.1 pd.concat() 合并规则 3 pd.Dataframe.drop() 删除行列的操作 4 pd.Dataframe 列操作 5 pd.Dataframe 行操作 5.1 sample_dataframe2.head(n2) 取前面的n行&…...

golang学习5

为结构体添加方法 异常处理过程...

【C语言】11月第二次测试 ing

文章目录 1.输入n名同学的成绩和学号,对成绩排序,输出对应学号 要求重复的学号重新输入 计算n名同学的平均值,对小于60分的同学删除分数 大于60分的同学输出:优秀:几人,良好:几人,中…...

行列式的理解与计算:线性代数中的核心概念

开发领域:前端开发 | AI 应用 | Web3D | 元宇宙 技术栈:JavaScript、React、ThreeJs、WebGL、Go 经验经验:6 年 前端开发经验,专注于图形渲染和 AI 技术 开源项目:github 简智未来、数字孪生引擎、前端面试题 大家好&a…...

按出生日期排序(结构体专题)

题目描述 送人玫瑰手有余香,小明希望自己能带给他人快乐,于是小明在每个好友生日的时候发去一份生日祝福。小明希望将自己的通讯录按好友的生日排序排序,这样就查看起来方便多了,也避免错过好友的生日。为了小明的美好愿望&#x…...

【C++】拆分详解 - 多态

文章目录 一、概念二、定义和实现1. 多态的构成条件2. 虚函数2.1 虚函数的重写/覆盖2.2 虚函数重写的两个例外 3. override 和 final关键字4. 重载/重写/隐藏的对比5. 例题 三、纯虚函数和抽象类四、多态的原理1. 虚函数表2. 实现原理3. 动态绑定和静态绑定 总结 一、概念 多态…...

Python世界:力扣题解875,珂珂爱吃香蕉,中等

Python世界:力扣题解875,珂珂爱吃香蕉,中等 任务背景思路分析代码实现坑点排查测试套件本文小结 任务背景 问题来自力扣题目875 Koko Eating Bananas,大意如下: Koko loves to eat bananas. There are n piles of bana…...

Java设计模式 —— Java七大设计原则详解

文章目录 前言一、单一职责原则1、概述2、案例演示 二、接口隔离原则1、概述2、案例演示 三、依赖倒转原则1、概述2、案例演示 四、里氏替换原则1、概述2、案例演示 五、开闭原则1、概述2、案例演示 六、迪米特法则1、概述2、案例演示 七、合成/聚合复用原则1、概述2、组合3、聚…...

SpringBoot学习记录(六)配置文件参数化

SpringBoot学习记录(六)配置文件参数化 一、参数提取到配置文件中二、yml配置文件三、ConfigurationProperties注解实现批量属性注入 一、参数提取到配置文件中 定义在代码中的参数的值分散在各个不同的文件中,不便于后期维护管理&#xff0…...

android 使用MediaPlayer实现音乐播放--获取音乐数据

前面已经添加了权限&#xff0c;有权限后可以去数据库读取音乐文件&#xff0c;一般可以获取全部音乐、专辑、歌手、流派等。 1. 获取全部音乐数据 class MusicHelper {companion object {SuppressLint("Range")fun getMusic(context: Context): MutableList<Mu…...

.net 8使用hangfire实现库存同步任务

C# 使用HangFire 第一章:.net Framework 4.6 WebAPI 使用Hangfire 第二章:net 8使用hangfire实现库存同步任务 文章目录 C# 使用HangFire前言项目源码一、项目架构二、项目服务介绍HangFire服务结构解析HangfireCollectionExtensions 类ModelHangfireSettingsHttpAuthInfoUs…...

第 22 章 - Go语言 测试与基准测试

在Go语言中&#xff0c;测试是一个非常重要的部分&#xff0c;它帮助开发者确保代码的正确性、性能以及可维护性。Go语言提供了一套标准的测试工具&#xff0c;这些工具可以帮助开发者编写单元测试、表达式测试&#xff08;通常也是指单元测试中的断言&#xff09;、基准测试等…...

VB.Net笔记-更新ing

目录 1.1 设置默认VS的开发环境为VB.NET&#xff08;2024/11/18&#xff09; 1.2 新建一个“Hello&#xff0c;world”的窗体&#xff08;2024/11/18&#xff09; 1.3 计算圆面积的小程序&#xff08;2024/11/18&#xff09; 显示/隐式 声明 &#xff08;2024/11/18&…...

centos 服务器 docker 使用代理

宿主机使用代理 在宿主机的全局配置文件中添加代理信息 vim /etc/profile export http_proxyhttp://127.0.0.1:7897 export https_proxyhttp://127.0.0.1:7897 export no_proxy"localhost,127.0.0.1,::1,172.171.0.0" docker 命令使用代理 例如我想在使用使用 do…...

python语言基础

1. 基础语法 Q: Python 中的变量与数据类型有哪些&#xff1f; A: Python 支持多种数据类型&#xff0c;包括数字&#xff08;整数 int、浮点数 float、复数 complex&#xff09;、字符串 str、列表 list、元组 tuple、字典 dict 和集合 set。每种数据类型都有其特定的用途和…...

Python中的Apriori库详解

文章目录 Python中的Apriori库详解一、引言二、Apriori算法原理与Python实现1、Apriori算法原理2、Python实现1.1、数据准备1.2、转换数据1.3、计算频繁项集1.4、提取关联规则 三、案例分析1、导入必要的库2、准备数据集3、数据预处理4、应用Apriori算法5、生成关联规则6、打印…...

MongoDB比较查询操作符中英对照表及实例详解

mongodb比较查询操作符中英表格一览表 NameDescription功能$eqMatches values that are equal to a specified value.匹配值等于指定值。$gtMatches values that are greater than a specified value.匹配值大于指定值。$gteMatches values that are greater than or equal to…...

掌上单片机实验室 – RT-Thread + ROS2 初探(25)

在初步尝试RT-Thread之后&#xff0c;一直在琢磨如何进一步感受它的优点&#xff0c;因为前面只是用了它的内核&#xff0c;感觉和FreeRTOS、uCOS等RTOS差别不大&#xff0c;至于它们性能、可靠性上的差异&#xff0c;在这种学习性的程序中&#xff0c;很难有所察觉。 RT-Threa…...

‌Kotlin中的?.和!!主要区别

目录 1、?.和!!介绍 2、使用场景和最佳实践 3、代码示例和解释 1、?.和!!介绍 ‌Kotlin中的?.和!!主要区别在于它们对空指针的处理方式。‌ ‌?.&#xff08;安全调用操作符&#xff09;‌&#xff1a;当变量可能为null时&#xff0c;使用?.可以安全地调用其方法或属性…...

iframe嵌入踩坑记录

iframe嵌入父子页面token问题 背景介绍 最近在做在平台A中嵌入平台B某个页面的需求&#xff0c;我负责的是平台B这边&#xff0c;使这个页面被嵌入后能正常使用。两个平台都实现了单点登录。 其实这是第二次做这个功能了&#xff0c;原本以为会很顺利&#xff0c;但没想到折腾…...