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

《深入理解双向链表:增删改查及销毁操作》

目录

一. 链表分类

1.1 单向和双向

1.2 带头和不带头

1.3 循环和不循环

二. 双向链表

2.1 双向链表的代码定义

2.2 双向链表的初始化

3.3 哨兵位

 三. 双向链表的增删改查与销毁

3.1 新节点的申请以及初始化

3.2 尾插

3.3 头插

3.4 尾删

3.5 头删

3.6 查找

3.7 指定位置后插入

3.8 指定位置删除

3.9 销毁

3.10 打印函数


一. 链表分类

链表从结构特点可以分为是否带头结点 是否是循环链表 是否是双向链表

因此可分为8类

其中双向链表就是 带头双向循环链表

1.1 单向和双向

单向链表

  • 单向链表:每个节点只包含两个部分,即数据域指向下一个节点的指针域 。这意味着节点之间的连接是单向的,只能从一个节点访问到它的下一个节点,链表的最后一个节点的指针指向空(通常用null表示)。

双向链表

  • 双向链表:每个节点除了数据域外,还包含两个指针域,一个指针指向前一个节点(前驱节点),另一个指针指向后一个节点(后继节点) 。因此,双向链表的节点可以同时访问它的前驱和后继节点,在链表两端,头节点的前驱指针和尾节点的后继指针通常指向空(非循环双向链表)。

1.2 带头和不带头

不带头链表

  • 不带头链表:没有额外的特殊头节点,链表的第一个节点就直接存储数据。在单链表中,第一个节点的指针指向第二个节点,最后一个节点指针为空;双向链表中,第一个节点前驱指针为空,后继指针指向第二个节点 。

带头链表

  • 带头链表:存在一个特殊的头节点,这个头节点通常不存储实际的数据 ,它的主要作用是作为链表的起始标志。在单链表中,头节点的指针指向第一个真正存储数据的节点;在双向链表中,头节点的后继指针指向第一个数据节点,前驱指针可以指向空(单链表改造的双向链表)或者指向尾节点(循环双向链表)。

1.3 循环和不循环

不循环链表

  • 不循环链表:也就是常规链表,链表的最后一个节点的指针指向空(null) ,标志着链表的结束,节点之间的连接没有形成环状。

循环链表

  • 循环链表:链表中最后一个节点的指针不是指向空(null),而是指向链表的头节点,从而形成一个环形结构。它可以基于单链表形成循环单链表,也可以基于双向链表形成循环双向链表。在循环双向链表中,不仅尾节点的后继指针指向头节点,头节点的前驱指针还会指向尾节点 。

实际中最常用的其实还是两种:单链表(不带头单向不循环链表)和双向链表(带头双向循环链表)

二. 双向链表

结构特点

双向链表的每个节点除了存储数据元素外,还包含两个指针域:

  • 前驱指针(prev):用于指向该节点的前一个节点。
  • 后继指针(next):用于指向该节点的后一个节点 。

在双向链表的开头有一个头节点,头节点的前驱指针通常为空(如果不是循环双向链表);在链表末尾有一个尾节点,尾节点的后继指针为空(同样在非循环双向链表中 )。而循环双向链表中,头节点的前驱指针指向尾节点,尾节点的后继指针指向头节点。

2.1 双向链表的代码定义

typedef int LTDataType;
typedef struct ListNode
{LTDataType data;//前驱指针,指向前一个指针struct ListNode* prev;//后继指针,指向后一个指针struct ListNode* next;
}ListNode;

2.2 双向链表的初始化

//头节点初始化
void LTInit(ListNode** pphead)
{ListNode* ph = (ListNode*)malloc(sizeof(ListNode));if (ph==NULL){perror("malloc fail!");exit(1);}*pphead = ph;(*pphead)->data = -1;//任意取值(*pphead)->prev = *pphead;(*pphead)->next = *pphead;
}

我们称呼双向链表的头节点为哨兵位 下面我来向大家解释一下哨兵位的作用和意义

3.3 哨兵位

在数据结构(尤其是链表)中,哨兵位(Sentinel Node) 是一个不存储实际数据的节点,通常作为链表的 “占位符” 存在于链表的头部(或尾部),用于简化链表的操作逻辑,避免边界条件的特殊处理。

核心作用:消除边界判断

没有哨兵位时,操作链表(如插入、删除)需要额外判断 “链表是否为空”“操作的是不是头节点 / 尾节点” 等边界情况,逻辑复杂。
哨兵位的存在让链表在空状态和非空状态下的操作逻辑统一,无需特殊判断。

举例说明(双向链表)

以带头节点的双向链表为例,哨兵位即 “头节点”:

  • 哨兵位本身不存储有效数据,仅作为链表的固定起点。

  • 空链表时,哨兵位的prevnext指向NULL(普通链表)或自身(循环链表)。

  • 非空链表时,哨兵位的next指向第一个有效节点,最后一个有效节点的prev指向哨兵位(循环链表)。

优势对比

操作场景

无哨兵位

有哨兵位(哨兵节点)

插入第一个节点

需要判断头指针是否为NULL

直接插入到哨兵位之后,无需判断

删除最后一个节点

需要判断是否只剩一个节点

直接修改指针,逻辑统一

遍历边界

需判断next是否为NULL

循环链表中可通过哨兵位终止遍历

注:参数是二级指针而非一级指针的原因

LTInit 函数需要改变外部头指针变量本身的指向(从 NULL 变为指向新创建的哨兵位节点),而 C 语言中函数参数是值传递,因此必须通过二级指针才能实现对外部指针变量的修改。如果使用一级指针,无法将初始化后的节点地址 “传回” 给外部,导致初始化失败。

 三. 双向链表的增删改查与销毁

在学习双向链表的增删查改之前 我们需要封装ListNode* LTBuyNode(LTDataType x)函数 用于申请新的节点 以及初始化新的节点

3.1 新节点的申请以及初始化

ListNode* ByeLTNode(LTDataType x)
{ListNode* newcode = (ListNode*)malloc(sizeof(ListNode));newcode->data = x;newcode->next = newcode;newcode->prev = newcode;return newcode;
}

3.2 尾插

// 3.1 尾插:在链表尾部插入节点
void LTPushBack(ListNode* phead, LTDataType x) {assert(phead);  // 哨兵位不能为空ListNode* newNode = BuyLTNode(x);ListNode* tail = phead->prev;  // 尾节点是哨兵位的前驱// 调整指针关系tail->next = newNode;newNode->prev = tail;newNode->next = phead;phead->prev = newNode;
}
  • 找到当前尾节点(哨兵位的prev
  • 新节点插入到尾节点和哨兵位之间
  • 时间复杂度:O (1)(无需遍历找尾)

3.3 头插

// 3.2 头插:在链表头部(哨兵位后)插入节点
void LTPushFront(ListNode* phead, LTDataType x) {assert(phead);ListNode* newNode = BuyLTNode(x);ListNode* first = phead->next;  // 第一个有效节点// 调整指针关系phead->next = newNode;newNode->prev = phead;newNode->next = first;first->prev = newNode;
}
  • 找到当前第一个有效节点(哨兵位的next
  • 新节点插入到哨兵位和第一个有效节点之间
  • 时间复杂度:O (1)

3.4 尾删

// 3.3 尾删:删除链表尾部节点
void LTPopBack(ListNode* phead) {assert(phead);assert(phead->next != phead);  // 链表不能为空(只有哨兵位)ListNode* tail = phead->prev;  // 要删除的尾节点ListNode* prevTail = tail->prev;  // 尾节点的前驱// 调整指针关系prevTail->next = phead;phead->prev = prevTail;free(tail);tail = NULL;  // 避免野指针
}
    • 找到尾节点和尾节点的前驱
    • 断开尾节点与前后节点的连接并释放
    • 时间复杂度:O (1)

    3.5 头删

    void LTPopFront(ListNode* phead) {assert(phead);assert(phead->next != phead);  // 链表不能为空ListNode* first = phead->next;  // 要删除的第一个节点ListNode* second = first->next;  // 第二个节点// 调整指针关系phead->next = second;second->prev = phead;free(first);first = NULL;
    }
    • 找到第一个有效节点和第二个有效节点
    • 断开第一个节点与前后节点的连接并释放
    • 时间复杂度:O (1)

    3.6 查找

    // 4.1 查找:查找值为x的节点,返回第一个找到的节点指针,找不到返回NULL
    ListNode* LTFind(ListNode* phead, LTDataType x) {assert(phead);ListNode* cur = phead->next;  // 从第一个有效节点开始查找while (cur != phead) {  // 循环结束条件:回到哨兵位if (cur->data == x) {return cur;  // 找到,返回节点指针}cur = cur->next;}return NULL;  // 未找到
    }
    • 从第一个有效节点开始遍历,直到回到哨兵位
    • 找到目标值时返回对应节点指针,未找到返回 NULL
    • 时间复杂度:O (n),需要遍历链表

    3.7 指定位置后插入

    // 4.2 指定位置后插入:在pos节点之后插入新节点
    void LTInsertAfter(ListNode* pos, LTDataType x) {assert(pos);  // pos不能为NULLListNode* newNode = BuyLTNode(x);ListNode* nextNode = pos->next;  // 记录pos的下一个节点// 调整指针关系pos->next = newNode;newNode->prev = pos;newNode->next = nextNode;nextNode->prev = newNode;
    }
    • 在给定节点pos的后面插入新节点
    • 只需调整pos、新节点和pos下一个节点的指针关系
    • 时间复杂度:O (1),已知位置时无需遍历
    • 注意:pos不能是 NULL,需要通过断言确保

    3.8 指定位置删除

    void LTErase(ListNode* pos) {assert(pos);  // pos不能为NULLListNode* prevNode = pos->prev;  // pos的前一个节点ListNode* nextNode = pos->next;  // pos的后一个节点// 调整指针关系,跳过pos节点prevNode->next = nextNode;nextNode->prev = prevNode;// 释放pos节点内存free(pos);pos = NULL;  // 避免野指针
    }
    • 直接删除pos指向的节点
    • 通过pos->prevpos->next找到前后节点,调整它们的指针关系
    • 释放pos节点的内存,避免内存泄漏
    • 时间复杂度:O (1),已知位置时无需遍历

    3.9 销毁

    void LTDestroy(ListNode** pphead) {assert(pphead && *pphead);  // 链表必须存在ListNode* cur = (*pphead)->next;ListNode* next = NULL;// 遍历释放所有有效节点while (cur != *pphead) {next = cur->next;  // 先记录下一个节点free(cur);cur = next;}// 释放哨兵位free(*pphead);*pphead = NULL;  // 避免野指针
    }
    • 需要使用二级指针,因为要将外部的头指针置为 NULL
    • 先遍历释放所有有效节点,再释放哨兵位
    • 确保所有动态分配的内存都被释放,避免内存泄漏
    • 时间复杂度:O (n),需要遍历整个链表

    3.10 打印函数

    // 打印链表(用于测试)
    void LTPrint(ListNode* phead) {assert(phead);ListNode* cur = phead->next;printf("哨兵位 -> ");while (cur != phead) {printf("%d -> ", cur->data);cur = cur->next;}printf("哨兵位\n");
    }

    以上就是全部内容 希望能为了提供些许帮助 谢谢

    http://www.xdnf.cn/news/1462177.html

    相关文章:

  • 贪吃蛇鱼小游戏抖音快手微信小程序看广告流量主开源
  • 架构性能优化三板斧:从10秒响应到毫秒级的演进之路
  • VSCode+MobaXterm+X11可视化界面本地显示
  • pydantic定义llm response数据模型
  • A股大盘数据-20250905 分析
  • HPL2.3安装
  • 期权卖方的收益和损失如何计算?
  • K8S删除命名空间卡住一直Terminating状态
  • 【小白笔记】命令不对系统:无法将‘head’项识别为 cmdlet、函数、脚本文件或可运行程序的名称
  • 【GEOS-Chem 输入数据】使用 AWS CLI 访问 GEOS-Chem 数据
  • LangChain实战(十六):构建基于SQL数据库的数据分析Agent
  • 深度学习——残差神经网路
  • 鸿蒙NEXT自定义能力详解:从基础使用到高级技巧
  • IDE mac M芯片安装报错:如何解决“InsCode.app 已损坏”,无法打开
  • 从零开始:用uv构建并发布一个Python CLI应用,集成CI/CD自动化发布与Docker容器化部署
  • 码农的“必修课”:深度解析Rust的所有权系统(与C++内存模型对比)
  • PCDN双系统赋能企业
  • LeetCode 2749.得到整数零需要执行的最少操作数:很独特的一道数学题(多公式硬讲——一步步还真能看懂)
  • 计算机网络7 第七章 网络安全
  • Graphpad 绘图(二):小鼠生存曲线绘制与数据记录分析详解
  • Windows 部署 Gerrit 与 Apache24 配置
  • 【传奇开心果系列】Flet框架实现的搜索引擎搜索关键词建议提示和自动完成自定义组件模板特色和实现原理深度解析
  • 无人机小目标检测新SOTA:MASF-YOLO重磅开源,多模块协同助力精度飞跃
  • [特殊字符] 香蕉超市|Nano Bananary|ZHO|已开源
  • 大数据毕业设计选题推荐-基于大数据的分化型甲状腺癌复发数据可视化分析系统-Spark-Hadoop-Bigdata
  • 85 printk 输出丢失数据
  • 分布式专题——1.1 Redis单机、主从、哨兵、集群部署
  • 解决 Apache/WAF SSL 证书链不完整导致的 PKIX path building failed 问题
  • 还在为第三方包 bug 头疼?patch-package 让你轻松打补丁!
  • 时间轮算法在workerman心跳检测中的实战应用