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的前一个节点
文章到这里就结束了,感谢你的阅读,喜欢的话记得三连

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java小白成长记(创作笔记二)

目录 序言 思维导图 续 用户登录/注册 数据表 实体层 持久层 服务层 认证与授权 整合springsecurity controller注册测试 controller登录测试 跨域解决 方法 Java小白成长记(创作笔记一) Java小白成长记(创作笔记二)…

案例研究|阿特斯的JumpServer分布式部署和多组织管理实践

苏州阿特斯阳光电力科技有限公司(以下简称为阿特斯)是一家集太阳能光伏组件制造和为全球客户提供太阳能应用产品研发、设计、制造、销售的专业公司。 阿特斯集团总部位于加拿大,中国区总部位于江苏省苏州市。通过全球战略和多元化的市场布局…

20241123-四元数高阶奇异值分解-(1)

四元数高阶奇异值分解及其在彩色图像处理中的应用-(1) 📔 声明 🇨🇳 : 1️⃣ 📃 原文网址链接: 四元数高阶奇异值分解及其在彩色图像处理中的应用 - ScienceDirect 🔗 Quaternion … image processing (arxiv.org) ​ …

游戏引擎学习第20天

视频参考:https://www.bilibili.com/video/BV1VkBCYmExt 解释 off-by-one 错误 从演讲者的视角:对代码问题的剖析与修复过程 问题的起因 演讲者提到,他可能无意中在代码中造成了一个错误,这与“调试时间标记索引”有关。他发现了一个逻辑问题…

python开发之Linux

文章目录 1. 基础2. 进阶链接压缩/解压缩 文件权限用户远程操作编辑文件软件安装 1. 基础 # 查看当前目录下文件 ls# 查看当前目录 pwd# 清除界面内容 clear# 切换目录 cd# 创建目录 mkdir# 创建文件 touch 文件 vi 文件# 强制删除 rm -rf # 复制文件 cp 复制文件 复制文件路径…

Docker2:docker快速入门(部署MySQL)

欢迎来到“雪碧聊技术”CSDN博客! 在这里,您将踏入一个专注于Java开发技术的知识殿堂。无论您是Java编程的初学者,还是具有一定经验的开发者,相信我的博客都能为您提供宝贵的学习资源和实用技巧。作为您的技术向导,我将…

oracle的静态注册和动态注册

oracle的静态注册和动态注册 静态注册: 静态注册 : 指将实例的相关信息手动告知 listener 侦 听 器 , 可以使用netmgr,netca,oem 以及直接 vi listener.ora 文件来实现静态注册,在动态注册不稳定时使用,特点是:稳定&…

杰发科技AC7840——EEP中RAM的配置

sample和手册中示例代码的sram区地址定义不一样 这个在RAM中使用没有限制,根据这个表格留下足够空间即可 比如需要4096字节的eep空间,可以把RAM的地址改成E000,即E000-EFFF,共4096bytes即可。

洛谷 P1616 疯狂的采药 C语言 记忆化搜索

题目: https://www.luogu.com.cn/problem/P1616?contestId215526 完全背包问题,最后一个超出空间了。完全背包和就是无限次的拿,公式跟01背包差不多。 但是,只有当前能拿和拿不下,换下一个。注意要处理好边界条件。…

分布式 Data Warebase - 构筑 AI 时代数据基石

导读:作者以人类世界一个信息层次模型 DIKW 为出发点,引出对计算机世界(系统)处理数据过程的介绍。接着以一个民宿平台数据架构随业务发展而不断演进的过程,展示了这场信息革命中,在具体应用场景下&#xf…

zotero7 插件使用

zotero style 1、下载地址 Zotero 插件商店 | Zotero 中文社区 2、配置 在工具插件里 3、配置 style 进入高级→设置编辑器 查找 easy 设置完即可显示, 注1:easyscholar的密钥要自行申请注册,注册地址:easySchol…

使用 Elastic AI Assistant for Search 和 Azure OpenAI 实现从 0 到 60 的转变

作者:来自 Elastic Greg Crist Elasticsearch 推出了一项新功能:Elastic AI Assistant for Search。你可以将其视为 Elasticsearch 和 Kibana 开发人员的内置指南,旨在回答问题、引导你了解功能并让你的生活更轻松。在 Microsoft AI Services…

CCF认证202406-02 | 矩阵重塑(其二)

题目背景 矩阵转置操作是将矩阵的行和列交换的过程。在转置过程中,原矩阵 A 的元素 aij​ 会移动到转置后的矩阵 AT 的 aji​ 的位置。这意味着 A 的第 i 行第 j 列的元素在 AT 中成为了第 j 行第 i 列的元素。 例如,有矩阵 A 如下: A[abc…

【CSP CCF记录】201903-2第16次认证 二十四点

题目 样例1输入 10 934x3 54x5x5 7-9-98 5x6/5x4 3579 1x19-9 1x9-5/9 8/56x9 6x7-3x6 6x44/5 样例1输出 Yes No No Yes Yes No No No Yes Yes 样例1解释 思路 参考:CCF小白刷题之路---201903-2 二十四点(C/C 100分)_ccf认证小白-CSDN博客 …

docker 容器运行Ruoyi-cloud

1,linux系统安装openjdk1.8,mvn,dokcer,node,git 2,拉取代码 1)查看gitee仓库地址 2)创建/app文件夹,进入app目录 mkdir /app cd /app 3)clone代码 4)修改配置文件中nacos地址 # 修改注…

浮点数的表示—IEEE754标准

浮点数的表示—IEEE754标准 引言 我们知道,在计算机中,数字以0和1组成的二进制序列来表示。但是,对于非常大的数字以及非常接近0的数字,简单的存储方式往往会造成精度的丢失。 为了解决这个问题,提供更高效的浮点数…

Window脚本自动化uiautomation详解_番茄出品

Window脚本自动化uiautomation详解_番茄出品 start 有时候pc端电脑,会有一些重复操作,希望能够通过代码实现这些操作。尝试了好几个库,但是识别准确率很低,在苦苦寻找之后,发现一个非常好用的 python 库 &#xff1a…

Java技术复习提升 11 常用类

第11章 常用类 1 包装类 不同包装类都继承自Object类 Serialiazble接口表示该类表示序列化 Comparable接口用于定义自然顺序 包装类和基本数据的转换 jdk5之前手动装箱拆箱 jdk5之后自动装箱拆箱 自动装箱底层调用的是valueof方法 拆箱仍然是intvalue方法 public class Inte…

Oracle - 多区间按权重取值逻辑 ,分时区-多层级-取配置方案(三)

本篇紧跟第一篇, 和 第二篇无关 Oracle - 多区间按权重取值逻辑 ,分时区-多层级-取配置方案 Oracle - 多区间按权重取值逻辑 ,分时区-多层级-取配置方案(二) 先说需求: 某业务配置表,按配置的时间区间及组织层级取方…

DASCTF 2024 10月 Reverse 完成笔记 附题目

题目链接: https://github.com/Airrcat/long_long/tree/main/DASCTF_2024_10 ezre 查PE 32位无壳 开始分析 看起来很像加壳了 字符串未有暴露信息,但是段中有一个themida 发现是一个壳,直接去找脱壳机 一些脱壳工具(Magicmida)是…