尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

带哨兵位的双向链表

带哨兵位的双向链表 一、简介上一篇我们实现了顺序表和单链表但是我们发现了一个问题链表的增、删、改都需要二级指针来操作为了解决这个麻烦今天引入一个叫哨兵位的家伙。为什么需要哨兵位双向链表是数据结构中的基础但普通双向链表有一个令人头疼的问题边界条件处理繁琐。哨兵位Sentinel Node核心思想是引入一个不存储有效数据的“哑结点”作为链表的固定端点让链表永远不为空。这样一来所有结点包括首元结点和尾结点都拥有前驱和后继插入删除操作不再需要特判边界。二、双向循环链表核心特性1. 结构特点每个节点包含前驱指针(prve)、后继指针(next)、数据域(data)带头结点头结点不存储有效数据统一空链表和非空链表的操作逻辑避免特殊判空处理循环结构尾节点的next指向头结点头结点的prve指向尾节点链表首尾闭环2. 时间复杂度优势头插、头删、尾插、尾删O(1)查找、遍历O(N)链式结构固有特性无法优化指定位置插入/删除O(1)已知pos节点时三、前置头文件与结构体定义List.h#includestdio.h #includestdlib.h #includeassert.h #includestdbool.h typedef int LTDataType; typedef struct ListNode { LTDataType data; struct ListNode* next; struct ListNode* prve; }LTNode; //初始化 LTNode* LTInit(); //尾插双链表 void LTPushBack(LTNode* phead, LTDataType x); //打印双链表 void LTPrint(LTNode* phead); //头插双链表 void LTPushFront(LTNode* phead, LTDataType x); //尾删双链表 void LTPopBack(LTNode* phead); //头删双链表 void LTPopFront(LTNode* phead); //查找 LTNode* LTFind(LTNode* phead, LTDataType x); //在pos之后的位置插入数据 void LTInsertBack(LTNode* pos, LTDataType x); //在pos之前的位置插入数据 void LTInsertFront(LTNode* pos, LTDataType x); //删除pos位置的节点 void LTErase(LTNode* pos); //链表的销毁 void LTDesTroy(LTNode* phead);设计说明分离式编程使用typedef重命名结构体和数据类型代码通用性更强后续如需修改存储数据类型只需改动一处即可。四、核心功能接口完整实现与解析1. 节点空间申请LTBuyNode关键细节新节点初始化时让自身的前驱、后继都指向自己适配循环链表特性避免野指针。// 申请新节点空间并初始化 LTNode* LTBuyNode(LTDataType x) { // 动态申请节点内存 LTNode* newNode (LTNode*)malloc(sizeof(LTNode)); // 内存申请失败校验 if (!newNode) { perror(newNode fail!); // 打印系统错误信息 exit(1); // 终止程序 } // 赋值数据域 newNode-data x; // 新节点默认自闭环空节点状态 newNode-prve newNode-next newNode; return newNode; }2. 链表判空LTEmpty操作基于循环链表特性空链表的唯一判定条件头结点的后继指向自身。// 链表判空空返回true非空返回false bool LTEmpty(LTNode* phead) { // 带头结点空链表phead-next phead return phead-next phead; }3. 链表初始化LTInit操作创建头结点完成空链表初始化所有链表操作均基于头结点展开。// 初始化双向循环链表返回头结点地址 LTNode* LTInit() { // 头结点数据域无意义默认赋值-1 LTNode* pphead LTBuyNode(-1); return pphead; }4. 尾插数据LTPushBack操作利用循环链表特性phead-prve直接指向尾节点无需遍历O(1)效率完成尾插。// 链表尾插 void LTPushBack(LTNode* phead, LTDataType x) { assert(phead); // 断言头结点不能为空 LTNode* newNode LTBuyNode(x); // 建立新节点与原尾节点的关系 newNode-prve phead-prve; phead-prve-next newNode; // 建立新节点与头结点的关系完成闭环 newNode-next phead; phead-prve newNode; }5. 链表打印LTPrint操作从第一个有效节点开始遍历遍历至头结点终止打印所有有效数据。// 遍历打印链表所有有效数据 void LTPrint(LTNode* phead) { assert(phead); // 从第一个有效节点开始遍历 LTNode* pv phead-next; // 遍历终止条件回到头结点 while (pv ! phead) { printf(%d -, pv-data); pv pv-next; } printf(\n); }6. 头插数据LTPushFront操作在头结点和第一个有效节点之间插入新节点完成头插操作。// 链表头插 void LTPushFront(LTNode* phead, LTDataType x) { assert(phead); LTNode* newNode LTBuyNode(x); // 连接新节点与原第一个有效节点 newNode-next phead-next; phead-next-prve newNode; // 连接新节点与头结点 newNode-prve phead; phead-next newNode; }7. 尾删数据LTPopBack操作直接定位尾节点修改头尾指针关联释放尾节点内存删除后仍保持链表闭环。// 链表尾删 void LTPopBack(LTNode* phead) { // 断言链表不能为空空链表禁止删除 assert(!LTEmpty(phead)); // 定位尾节点 LTNode* del phead-prve; // 断开尾节点连接重新建立闭环 phead-prve del-prve; del-prve-next phead; // 释放内存避免内存泄漏 free(del); del NULL; }8. 头删数据LTPopFront操作删除第一个有效节点修正头结点与新首节点的指针关系。// 链表头删 void LTPopFront(LTNode* phead) { assert(!LTEmpty(phead)); // 定位第一个有效节点 LTNode* del phead-next; // 断开原首节点连接建立新链接 phead-next del-next; del-next-prve phead; free(del); del NULL; }9. 数据查找LTFind操作遍历链表匹配目标数据返回对应节点地址无匹配则返回NULL为后续插入、删除提供pos位置。// 查找值为x的节点返回节点地址 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; // 未找到目标节点 }10. 指定位置后插入LTInsertBack操作在pos节点后方插入新节点无需移动节点仅修改指针指向。// 在pos节点之后插入数据 void LTInsertBack(LTNode* pos, LTDataType x) { assert(pos); LTNode* newNode LTBuyNode(x); // 先连接新节点的前后指针 newNode-next pos-next; newNode-prve pos; // 再修改原后续节点和pos节点的指针 pos-next-prve newNode; pos-next newNode; }11. 指定位置前插入LTInsertFront操作在pos节点前方插入新节点适配更多自定义插入场景。// 在pos节点之前插入数据 void LTInsertFront(LTNode* pos, LTDataType x) { assert(pos); LTNode* newNode LTBuyNode(x); // 绑定新节点与pos、pos前驱节点的关系 newNode-next pos; newNode-prve pos-prve; // 修正原节点指针指向 pos-prve-next newNode; pos-prve newNode; }12. 指定节点删除LTErase操作删除任意已知pos节点通用性极强可配合LTFind实现按值删除。// 删除pos位置的节点 void LTErase(LTNode* pos) { assert(pos); // 跳过pos节点直接关联前后节点 pos-prve-next pos-next; pos-next-prve pos-prve; // 释放节点内存 free(pos); pos NULL; }13. 链表销毁LTDesTroy操作遍历释放所有有效节点头结点彻底回收内存杜绝内存泄漏。// 销毁整个链表释放所有内存 void LTDesTroy(LTNode* phead) { assert(phead); LTNode* pcur phead-next; // 遍历释放所有有效节点 while (pcur ! phead) { pcur pcur-next; free(pcur-prve); } // 释放头结点 free(phead); }五、核心易错点总结指针修改顺序插入节点时必须先绑定新节点的指针再修改原节点指针否则会丢失链表地址空链表保护删除操作必须判空空链表执行删操作会导致指针越界崩溃循环终止条件遍历终止条件必须是pcur ! phead不能用NULL否则会死循环内存释放所有malloc申请的节点必须手动free链表使用完毕必须调用销毁函数断言校验所有接口入参指针必须断言判空避免野指针操作六、整体总结双向循环链表是线性表中综合效率最高的链式结构对比单链表首尾操作从O(N)优化为O(1)支持双向遍历、任意位置快速插入删除。本文实现的代码封装完整、逻辑严谨包含工业级基础校验可直接用于课程设计、项目开发和算法刷题。核心优势概括结构闭环、操作统一、效率高效、通用性强。七、有哨兵位双链表对比无哨兵位单链表前文完整实现了带头哨兵位的双向循环链表也是工程开发中的最优写法。为了让大家彻底理解该结构的设计价值本节将它和无哨兵位普通单链表做全方位对比从结构本质、代码逻辑、边界处理、时间效率、适用场景五个维度深度剖析厘清两种链表的优劣与适用场景。7.1 基础结构对比无哨兵位单链表无额外头结点第一个节点即为有效数据节点链表首尾不闭环尾节点next指针置为NULL仅支持单向遍历。有哨兵位双向循环链表单独开辟一个哨兵头结点不存储有效数据链表首尾闭环每个节点均包含前驱、后继双指针支持双向遍历。7.2 核心操作边界逻辑对比这是两种结构最大的差异也是哨兵位结构的核心优势所在。无哨兵位单链表痛点所有首尾操作、空链表操作都需要特殊分支判断代码冗余且容易出错头插、头删需要单独更新链表头指针需区分空链表、单节点链表、多节点链表三种场景尾插、尾删必须遍历整个链表找到尾节点无法直接定位且需要判空防止空指针崩溃空链表与非空链表操作逻辑完全不同分支代码多维护成本高有哨兵位双链表优势无任何特殊边界判断无论链表为空、只有一个节点、还是多个节点增删操作逻辑完全统一头尾节点可通过phead-next、phead-prve直接O(1)定位无需遍历闭环结构杜绝野指针所有节点指针均有合法指向不存在NULL指针访问问题7.3 时间复杂度对比操作场景无哨兵位单链表有哨兵位双向循环链表头部插入/删除O(1)O(1)尾部插入/删除O(N)需遍历找尾O(1)直接通过头结点定位尾节点正向遍历O(N)O(N)逆向遍历不支持O(N)指定位置插入/删除O(N)需遍历找前驱O(1)已知pos节点可直接操作7.4 代码复杂度与稳定性对比无哨兵位单链表代码逻辑碎片化大量if-else分支处理边界情况新手极易遗漏空链表、单节点边界导致野指针、内存泄漏、程序崩溃等问题。代码复用性差每一个增删接口都需要重复编写边界判断逻辑。有哨兵位双链表接口逻辑高度统一无冗余分支代码所有场景复用一套指针操作逻辑。仅初始化时多开辟一个哨兵节点极小的内存开销换来极高的代码稳定性和可读性非常适合工程级开发。7.5 两种结构优缺点总结无哨兵位单向单链表优点结构极简、内存开销最小无需额外开辟哨兵节点逻辑轻量化。缺点边界处理繁琐、尾部操作效率极低、不支持逆向遍历、代码容错率低、维护成本高。有哨兵位双向循环链表优点操作逻辑统一无边界、首尾操作O(1)高效、支持双向遍历、代码健壮性强、几乎无野指针问题。缺点多占用一个哨兵节点内存可忽略、节点结构稍复杂双指针。7.6 适用场景选型​​​​​选用无哨兵单链表仅适用于只做头插头删、极少尾部操作、极致轻量化的简单场景如简单数据缓存、临时数据过渡。选用哨兵位双向循环链表绝大多数正式开发场景需要频繁增删数据、双向遍历、随机位置修改的场景如内核链表、容器底层、任务队列、列表数据管理等。
返回列表