深入剖析带头循环双向链表的实现与应用

引言

场景描述

想象一个 环形地铁线路(如深圳地铁11号线),这条线路首尾相连,列车可以顺时针或逆时针循环行驶。为了方便管理,地铁系统设置了一个 “虚拟调度中心”(头节点),它不承载乘客,但作为整个环线的逻辑起点和终点,用于监控和协调所有站点的运行。


映射关系
双向循环链表结构地铁环线系统
头节点(不存储数据)虚拟调度中心(无乘客上下车)
数据节点地铁站点(如“沙井站”、“马安山站”)
next 指针顺时针方向的下一个站点
prev 指针逆时针方向的上一个站点
循环结构线路首尾相连,列车可双向循环行驶

一、数据结构定义

typedef int LTDataType;
typedef struct ListNode {LTDataType data;struct ListNode* next;struct ListNode* prev;
} LTNode;
  • 节点结构
    • data:存储节点数据
    • next:指向下一个节点
    • prev:指向前一个节点
  • 循环特性
    • 头节点的next指向第一个有效节点
    • 头节点的prev指向最后一个节点
    • 形成闭环结构(头节点自环表示空链表)

二、核心函数实现详解

1. 节点创建函数 LTBuyNode
LTNode* LTBuyNode(LTDataType x) {LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL) {perror("malloc fail!");exit(1);}newnode->data = x;newnode->next = newnode->prev = newnode; // 初始自环return newnode;
}
  • 功能:动态分配内存创建新节点。

  • 细节

    • 数据域初始化为 x

    • nextprev 指针初始指向自身,形成自环结构。

    • 若内存分配失败,程序直接终止(exit(1))。


2. 链表初始化 LTInit
LTNode* LTInit() {LTNode* phead = LTBuyNode(-1); // 头节点数据为-1return phead;
}
  • 功能:创建带头节点的空链表。

  • 细节

    • 头节点数据为 -1nextprev 均指向自身。

    • 空链表仅含头节点,逻辑上表示链表为空。


3. 打印链表 LTPrint
void LTPrint(LTNode* phead) {LTNode* pcur = phead->next;while (pcur != phead) {printf("%d -> ", pcur->data);pcur = pcur->next;}printf("\n");
}
  • 功能:遍历链表并打印所有数据节点(不打印头节点)。

  • 细节

    • 从头节点的下一个节点开始遍历,直到回到头节点结束。

    • 输出格式为 1 -> 2 -> 3 -> ...,末尾换行。


4. 尾插法 LTPushBack
void LTPushBack(LTNode* phead, LTDataType x) {assert(phead);LTNode* newnode = LTBuyNode(x);newnode->prev = phead->prev; // 新节点prev指向原尾节点newnode->next = phead;       // 新节点next指向头节点phead->prev->next = newnode; // 原尾节点的next指向新节点phead->prev = newnode;       // 头节点的prev指向新节点
}
  • 功能:在链表尾部插入新节点。

  • 指针调整步骤

    1. 新节点的 prev 指向原尾节点。

    2. 新节点的 next 指向头节点。

    3. 原尾节点的 next 指向新节点。

    4. 头节点的 prev 指向新节点。


5. 头插法 LTPushFront
void LTPushFront(LTNode* phead, LTDataType x) {assert(phead);LTNode* newnode = LTBuyNode(x);newnode->next = phead->next; // 新节点next指向原首节点newnode->prev = phead;       // 新节点prev指向头节点phead->next->prev = newnode; // 原首节点的prev指向新节点phead->next = newnode;       // 头节点的next指向新节点
}
  • 功能:在链表头部插入新节点。

  • 指针调整步骤

    1. 新节点的 next 指向原首节点。

    2. 新节点的 prev 指向头节点。

    3. 原首节点的 prev 指向新节点。

    4. 头节点的 next 指向新节点。


6. 链表判空 LTEmpty
bool LTEmpty(LTNode* phead) {assert(phead);return phead->next == phead; // 仅头节点自环时为空
}
  • 功能:判断链表是否为空。

  • 返回值

    • true:链表为空(仅头节点存在)。

    • false:链表至少有一个数据节点。


7. 尾删法 LTPopBack
void LTPopBack(LTNode* phead) {assert(!LTEmpty(phead));     // 链表非空才能删除LTNode* del = phead->prev;   // 待删除的尾节点del->prev->next = phead;     // 尾节点前驱的next指向头节点phead->prev = del->prev;     // 头节点的prev指向尾节点前驱free(del);del = NULL;                  // 局部指针置空(不影响外部)
}
  • 功能:删除链表尾节点。

  • 细节

    • 断言确保链表非空。

    • 调整尾节点前驱的指针,跳过尾节点。

    • 释放尾节点内存,局部指针 del 置空。


8. 头删法 LTPopFront
void LTPopFront(LTNode* phead) {assert(!LTEmpty(phead));LTNode* del = phead->next;   // 待删除的首节点del->next->prev = phead;     // 首节点后继的prev指向头节点phead->next = del->next;     // 头节点的next指向首节点后继free(del);del = NULL;
}
  • 功能:删除链表首节点。

  • 操作步骤:类似尾删法,调整指针后释放内存。


9. 查找节点 LTFind
LTNode* LTFind(LTNode* phead, LTDataType x) {assert(phead);LTNode* pcur = phead->next;while (pcur != phead) {      // 遍历数据节点if (pcur->data == x) return pcur;pcur = pcur->next;}return NULL;                 // 未找到返回NULL
}
  • 功能:查找第一个值为 x 的节点。

  • 返回值:找到返回节点指针,否则返回 NULL


10. 在指定位置后插入 LTInsert
void LTInsert(LTNode* pos, LTDataType x) {assert(pos);LTNode* newnode = LTBuyNode(x);newnode->next = pos->next;   // 新节点next指向pos的后继newnode->prev = pos;         // 新节点prev指向pospos->next->prev = newnode;   // pos后继的prev指向新节点pos->next = newnode;         // pos的next指向新节点
}
  • 功能:在 pos 节点后插入新节点。

  • 应用场景:结合 LTFind 实现任意位置插入。


11. 在指定位置前插入 LTInsertFront
void LTInsertFront(LTNode* pos, LTDataType x) { assert(pos);LTNode* newnode = LTBuyNode(x);newnode->next = pos;         // 新节点next指向posnewnode->prev = pos->prev;   // 新节点prev指向pos的前驱pos->prev->next = newnode;   // pos前驱的next指向新节点pos->prev = newnode;         // pos的prev指向新节点
}
  • 功能:在 pos 节点前插入新节点。


12. 删除指定节点 LTErase
void LTErase(LTNode* pos) {assert(pos);pos->prev->next = pos->next; // pos前驱的next指向pos的后继pos->next->prev = pos->prev; // pos后继的prev指向pos的前驱free(pos);pos = NULL; // 局部指针置空(外部指针需手动置空)
}
  • 功能:删除 pos 节点。

  • 注意事项

    • pos 不能是头节点,否则链表结构被破坏。

    • 调用后,外部指向 pos 的指针变为野指针,需手动置空。


13. 销毁链表 LTDesTroy
void LTDesTroy(LTNode** phead) {LTNode* pcur = (*phead)->next;while (pcur != *phead) {     // 遍历释放所有数据节点LTNode* next = pcur->next;free(pcur);pcur = next;}free(*phead);                // 释放头节点*phead = NULL;               // 头指针置空
}
  • 功能:销毁整个链表,释放所有内存。

  • 设计

    • 使用二级指针确保外部头指针被置空。

    • 避免内存泄漏和野指针问题。


测试用例解析 test01

void test01() {LTNode* plist = LTInit();            // 初始化空链表LTPushBack(plist, 1);                // 尾插1LTPushBack(plist, 2);                // 尾插2 → 链表: 1 -> 2LTPushBack(plist, 3);                // 尾插3 → 链表: 1 -> 2 -> 3LTPushBack(plist, 4);                // 尾插4 → 链表: 1 -> 2 -> 3 -> 4LTPrint(plist);                      // 输出: 1 -> 2 -> 3 -> 4LTPushFront(plist, 0);               // 头插0 → 链表: 0 -> 1 -> 2 -> 3 -> 4LTPrint(plist);LTPopBack(plist);                    // 尾删4 → 链表: 0 -> 1 -> 2 -> 3LTPrint(plist);LTPopFront(plist);                   // 头删0 → 链表: 1 -> 2 -> 3LTPrint(plist);LTNode* pos = LTFind(plist, 2);      // 查找值为2的节点if (pos) {LTInsert(pos, 5);                // 在2后插入5 → 链表: 1 -> 2 -> 5 -> 3LTPrint(plist);}LTDesTroy(&plist);                   // 销毁链表,plist置NULL
}
  • 验证操作

    • 初始化、尾插、头插、尾删、头删、查找、指定位置插入、销毁。

    • 通过打印验证每一步操作的正确性

四、关键技术点总结

技术点说明
头节点设计简化边界条件处理,统一头插尾插操作
循环链表特性通过头节点的 prev 指针快速访问尾节点
指针操作技巧插入需修改 4 个指针,删除需修改 2 个指针
内存管理使用LTBuyNode统一分配内存,LTDesTroy彻底释放内存
安全性设计assert参数校验,内存释放后置 NULL

五、典型应用场景

  1. LRU 缓存
    • 通过头尾指针快速访问最近使用和最久未使用的节点
  2. 双端队列
    • 支持 O (1) 时间复杂度的头尾操作
  3. 文件系统 inode 管理
    • 快速定位文件的前后节点
  4. 哈希表冲突处理
    • 双向链表便于删除操作

六、扩展学习建议

  1. 实现链表逆序操作
  2. 尝试合并两个有序链表
  3. 研究跳表(Skip List)数据结构
  4. 了解 Linux 内核中的链表实现

最后附上全部代码:

代码List.h

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>//双向链表的结构
typedef int LTDataType;
typedef struct  ListNode {LTDataType data;struct ListNode* next;struct ListNode* prev;
}LTNode;//打印链表
void LTPrint(LTNode* phead);//双向链表但初始化
LTNode* LTInit();//销毁数据表
void LTDesTroy(LTNode** phead);
//void LTDataTroy(LTNode* phead); //也可也传一级指针不过,最后得自己NULL一下,// 尾插
void LTPushBack(LTNode* phead, LTDataType x);// 头插
void LTPushFront(LTNode* phead, LTDataType x);//只有一个头结点的情况下,双向链表为空
bool LTEmpty(LTNode* phead);//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);//查找
LTNode* LTFind(LTNode* phead, LTDataType x);//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);//在pos位置之前插入数据
void LTInsertFront(LTNode* pos, LTDataType x);//删除pos位置的结点
void LTErase(LTNode* pos);

代码List.c

#include"List.h"//创建新的结点
LTNode* LTBuyNode(LTDataType x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));//判断新结点是否创建成功if (newnode == NULL){perror("malloc fail!");exit(1);}//成功就把值赋上和指针指向newnode->data = x;newnode->next = newnode->prev = newnode;//返回一级指针指向的地址return newnode;
}void LTPrint(LTNode* phead)
{//把头结点的下一个指向的地址赋给新创建的临时指针LTNode* pcur = phead->next;//遍历链表的数据,遍历到头结点结束while (pcur != phead){printf("%d -> ", pcur->data);//把pcur下一个地址赋给自己pcur = pcur->next;}printf("\n");
}LTNode* LTInit()
{LTNode* phead = LTBuyNode(-1);return phead;
}//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{//断言phead不为空assert(phead);//创建新的结点LTNode* newnode = LTBuyNode(x);//把新的结点prev指向头结点的prevnewnode->prev = phead->prev;//把新的next指向phead的地址newnode->next = phead;//把头结点的prev的结点的next结点指向新的结点phead->prev->next = newnode;//把头结点prev指向新的结点phead->prev = newnode;
}//头插
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;phead->next = newnode;
}//只有一个头结点的情况下,双向链表为空
bool LTEmpty(LTNode* phead)
{assert(phead);//判断 phead->next 是否等于 phead 来确定链表是否为空,若相等则返回 true,表示链表为空;否则返回 false。return phead->next == phead;
}//尾删
void LTPopBack(LTNode* phead)
{//对LTEmpty函数取反assert(!(LTEmpty(phead)));LTNode* del = phead->prev;del->prev->next = phead;phead->prev = del->prev;free(del);del = NULL;
}//头删
void LTPopFront(LTNode* phead)
{//对LTEmpty函数取反assert(!(LTEmpty(phead)));LTNode* del = phead->next;del->next->prev = phead;phead->next = del->next;free(del);del = NULL;
}
//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}
//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = LTBuyNode(x);newnode->next = pos->next;newnode->prev = pos;pos->next->prev = newnode;pos->next = newnode;}
//在pos位置之前插入数据
void LTInsertFront(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = LTBuyNode(x);newnode->next = pos;newnode->prev = pos->prev;pos->prev->next = newnode;pos->prev = newnode;}//删除pos位置的结点
void LTErase(LTNode* pos)
{assert(pos);pos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);pos = NULL;
}//销毁数据表
void LTDesTroy(LTNode** phead)
{LTNode* pcur = (*phead)->next;while (pcur != *phead){LTNode* next = pcur->next;free(pcur);pcur = next;}free(*phead);*phead = NULL;
}//void LTDataTroy(LTNode* phead)
//{
//    LTNode* pcur = phead->next;
//    while (pcur != phead)
//    {
//        LTNode* next = pcur->next;
//        free(pcur);
//        pcur = next;
//    }
//    free(phead);
//    phead = NULL;
//
//}

代码test.c

#include"List.h"void test01()
{LTNode* plist = LTInit();LTPushBack(plist, 1);LTPushBack(plist, 2);LTPushBack(plist, 3);LTPushBack(plist, 4);LTPrint(plist);LTPushFront(plist, 0);LTPrint(plist);LTPopBack(plist);LTPrint(plist);LTPopFront(plist);LTPrint(plist);LTNode* pos = LTFind(plist, 2);if (pos){LTInsert(pos, 5);LTPrint(plist);}LTDesTroy(&plist);
}int main()
{test01();return 0;
}

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

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

相关文章

DeepSeek Smallpond 在火山引擎 AI 数据湖的探索实践

资料来源&#xff1a;火山引擎-开发者社区 DeepSeek Smallpond 介绍 Smallpond 是一套由 DeepSeek 推出的 、针对 AI 领域&#xff0c;基于 Ray 和 DuckDB 实现的轻量级数据处理引擎&#xff0c;具有以下优点&#xff1a; 1.轻量级 2.高性能 3.支持规模大 4.无需运维 5.P…

Linux进程间的通信

进程间通信 1.进程间通信介绍2.匿名命名管道原理操作 1.进程间通信介绍 1.1 进程间通信目的&#xff1a;一个进程需要将他的数据发送给另一个进程&#xff0c;大家应该都多少接触过linux中的管道符"|"&#xff0c;这个符号就是用来多个命令执行&#xff0c;在Linux中…

直播预告 | TDgpt 智能体发布 时序数据库 TDengine 3.3.6 发布会即将开启

从海量监控数据&#xff0c;到工业、能源、交通等场景中实时更新的各类传感器数据&#xff0c;时序数据正在以指数级速度增长。而面对如此庞杂的数据&#xff0c;如何快速分析、自动发现问题、精准预测未来&#xff0c;成为企业数字化转型过程中的关键挑战。 TDengine 的答案是…

手撕FIO工具指南:从压测翻车到避坑实战

文章目录 手撕FIO工具指南&#xff1a;从压测翻车到避坑实战一、背景&#xff1a;一次FIO压测引发的惊魂夜二、FIO vs 其他IO工具&#xff1a;为何让人又爱又怕&#xff1f;三、安装指南&#xff1a;避开依赖地狱四、参数详解五、避坑指南&#xff1a;血泪经验总结六、安全压测…

智能汽车图像及视频处理方案,支持视频星轨拍摄能力

美摄科技作为智能汽车图像及视频处理领域的先行者&#xff0c;正以革新性的技术引领着行业的未来发展。美摄科技智能汽车图像及视频处理方案&#xff0c;一个集高效性、智能化、画质增强于一体的创新解决方案&#xff0c;旨在重塑智能汽车图像画质的新标准&#xff0c;并支持前…

B站左神算法课学习笔记(P7):图

目录 一、图的存储方式&#xff08;千奇百怪&#xff09; 1&#xff09;邻接表 2&#xff09;邻接矩阵 3&#xff09;其他 4&#xff09;推荐存储方式&#xff08;代码&#xff09; 二、图的遍历 &#xff08;1&#xff09;宽度优先遍历 &#xff08;2&#xff09;深度…

深度解析「前缀和」与「差分法」:高效算法的基石

深度解析前缀和与差分法&#xff1a;高效算法的基石 在计算机科学和数据处理领域&#xff0c;前缀和&#xff08;Prefix Sum&#xff09;与差分法&#xff08;Difference Method&#xff09;是两种基础且高效的算法技术。它们在处理数组的区间查询和区间修改操作时&#xff0c…

2-1 基本放大电路

放大的概念 mV →V mA→A 特征&#xff1a;放大功率&#xff08;电压与电流&#xff09;。 本质&#xff1a;能量在控制下的转换。&#xff08;外接供电电源&#xff09; 必要条件&#xff1a;有源元件&#xff08;能量控制原件&#xff09; 前提&#xff1a;不失真 测试的…

详解接口的常见请求方式

详解接口的常见请求方式 一、 常见接口请求方式1. GET2. POST3. PUT4. DELETE5. PATCH6. HEAD7. OPTIONS 二、 实现方法1. 前端实现2. 后端实现 三、 作用与主要区别四、 举例讲解1. 创建 Spring Boot 工程2. 添加依赖3. 编写 Controller 实现接口关键点说明 4. 启动与测试5. 总…

【附代码】【MILP建模】3D装箱问题(3D-Bin Packing Problem)

文章目录 相关教程相关文献问题描述建模思路——carton 方向平行轴建模方法&#xff08;9变量6约束&#xff09;平行轴建模方法&#xff08;4变量8约束&#xff09;枚举建模方法&#xff08;6变量1约束&#xff09; 建模思路——carton 位置平行轴建模方法枚举建模方法 Bin长宽…

【计算机网络中的奈氏准则与香农定理】

文章目录 一、前言二、奈氏准则1. 概念2. 奈氏准则公式3. 奈氏准则的意义 三、香农定理1. 概念2. 香农定理公式3. 香农定理的意义 四、奈氏准则与香农定理的对比五、应用示例1. 奈氏准则示例2. 香农定理示例 六、总结 一、前言 在计算机网络中&#xff0c;数据的传输速率与信道…

【C++】回调函数和回调对象

文章目录 回调可调用对象函数指针作回调函数对象作回调函数对象的使用std::function【C11】作回调使用 【C11】Lambda表达式作回调【C11】bind对象作回调std::bind的使用作回调使用 回调 当发生某种事件时需要调用或触发另一个事件即为回调&#xff0c;回调的核心即为将可调用…

DeepSeek助力文案,智能音箱如何改变你的生活?

你好&#xff0c;我是三桥君 你有没有为写智能音箱的宣传文案而抓耳挠腮过&#xff1f;三桥君在这方面可是有些感想&#xff0c;今天就来给你唠唠怎么用DeepSeek写出超赞的智能音箱宣传文案。 首先&#xff0c;你得给DeepSeek喂足“料”。这就好比做饭&#xff0c;你得准备好各…

【区块链安全 | 第一篇】密码学原理

文章目录 1.哈希函数1.1 哈希函数的性质1.2 常见哈希算法1.3 Merkle Tree&#xff08;默克尔树&#xff09;1.4 HMAC&#xff08;哈希消息认证码&#xff09; 2. 公钥密码学2.1 对称加密 vs 非对称加密2.2 RSA 算法2.3 ECC&#xff08;椭圆曲线密码学&#xff09;2.4 Diffie-He…

基于websocketpp实现的五子棋项目

该博客对于学完C和linux操作系统&#xff0c;但不知道如何用C开发项目&#xff0c;已经不知道C如何使用第三方库的人来说一定很有帮助&#xff0c;请耐心看完&#xff01; 先看一下游戏会显示的前端界面&#xff0c;对理解这个游戏的前后端交互过程会有帮助 1. 开发环境 1.1 …

基于Redis分布锁+事务补偿解决数据不一致性问题

基于Redis的分布式设备库存服务设计与实现 概述 本文介绍一个基于Redis实现的分布式设备库存服务方案&#xff0c;通过分布式锁、重试机制和事务补偿等关键技术&#xff0c;保证在并发场景下库存操作的原子性和一致性。该方案适用于物联网设备管理、分布式资源调度等场景。 …

RK3568笔记八十: Linux 小智AI环境搭建

若该文为原创文章&#xff0c;转载请注明原文出处。 最近小智AI火了&#xff0c;韦老师出了 Linux 小智 AI 聊天机器人 版本&#xff0c;想移植到 RK3568上&#xff0c; 由于和韦老师硬件不同&#xff0c;所以需要交叉编译一些库&#xff0c;为后续移植做准备。 一、环境 1、…

C# SerialPort 使用详解

总目录 前言 在工业控制、物联网、嵌入式开发等领域&#xff0c;串口通信&#xff08;Serial Port Communication&#xff09;是连接串行设备&#xff08;如条码扫描器、GPS接收器等&#xff09;与计算机的重要手段。C# 提供了内置的 SerialPort 类&#xff0c;简化了串口开发…

3D点云的深度学习网络分类(按照作用分类)

1. 3D目标检测&#xff08;Object Detection&#xff09; 用于在点云中识别和定位目标&#xff0c;输出3D边界框&#xff08;Bounding Box&#xff09;。 &#x1f539; 方法类别&#xff1a; 单阶段&#xff08;Single-stage&#xff09;&#xff1a;直接预测3D目标位置&am…

LabVIEW 与 PLC 通讯的常见方式

在工业自动化和数据采集系统中&#xff0c;PLC&#xff08;可编程逻辑控制器&#xff09; 广泛用于控制和监测各种设备&#xff0c;而 LabVIEW 作为强大的图形化编程工具&#xff0c;常用于上位机数据处理和可视化。为了实现 LabVIEW 与 PLC 的高效通讯&#xff0c;常见的方法包…