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

引言

场景描述

想象一个 环形地铁线路(如深圳地铁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); // 头节点数据为-1
    return 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指向pos
    pos->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指向pos
    newnode->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);                // 尾插1
    LTPushBack(plist, 2);                // 尾插2 → 链表: 1 -> 2
    LTPushBack(plist, 3);                // 尾插3 → 链表: 1 -> 2 -> 3
    LTPushBack(plist, 4);                // 尾插4 → 链表: 1 -> 2 -> 3 -> 4
    LTPrint(plist);                      // 输出: 1 -> 2 -> 3 -> 4

    LTPushFront(plist, 0);               // 头插0 → 链表: 0 -> 1 -> 2 -> 3 -> 4
    LTPrint(plist);

    LTPopBack(plist);                    // 尾删4 → 链表: 0 -> 1 -> 2 -> 3
    LTPrint(plist);

    LTPopFront(plist);                   // 头删0 → 链表: 1 -> 2 -> 3
    LTPrint(plist);

    LTNode* pos = LTFind(plist, 2);      // 查找值为2的节点
    if (pos) {
        LTInsert(pos, 5);                // 在2后插入5 → 链表: 1 -> 2 -> 5 -> 3
        LTPrint(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指向头结点的prev
    newnode->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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值