数据结构 | 双循环链表

一、前言

单链表仅有后继指针next,只能单向访问,核心痛点的是找前驱节点需从头遍历、反向操作繁琐,且应对回文链表等算法题时逻辑笨重。双向链表新增前驱指针prior,专门解决这些问题,大幅提升灵活性和效率。

双向链表 vs 单链表

优点 :直接通过prior找前驱,无需遍历;已知节点删除效率O(1);支持反向遍历;简化回文判断、节点交换等算法题逻辑;头尾操作统一。

缺点:多一个指针,占用更多内存;插入/删除需修改两个指针,易出错;代码逻辑略复杂。

二、结构体设计

1. 结构体定义

cpp 复制代码
typedef struct DLNode {
 ELEMTYPE data;            //数据域
 struct DLNode* next;      //后继指针
 struct DLNode* prior;     //前驱指针
 } DLNode, * PDLNode;

2. 字段讲解: data存储数据;next实现正向遍历;prior实现反向遍历,是双向链表的核心。

**3. 设计说明:**采用带头结点设计,头结点不存有效数据,统一插入/删除逻辑,避免空链表等特殊情况的冗余判断。

三、函数接口

cpp 复制代码
// 工具函数
void Init_DoubleList(DLNode* plist);         // 初始化
bool IsEmpty(DLNode* plist);                 // 判空
int Get_Length(DLNode* plist);               // 获取长度
bool Clear(DLNode* plist);                   // 清空
bool Destroy1(DLNode* plist);                // 销毁(循环头删)
bool Destroy2(DLNode* plist);                // 销毁(双指针)
bool Show(DLNode* plist);                    // 打印
DLNode* Search(DLNode* plist, ELEMTYPE val); // 按值查找

// 核心函数(插入)
bool Insert_Head(DLNode* plist, ELEMTYPE val);         // 头插
bool Insert_Tail(DLNode* plist, ELEMTYPE val);         // 尾插
bool Insert_Pos(DLNode* plist, ELEMTYPE val, int pos); // 按位置插

// 核心函数(删除)
bool Del_Head(DLNode* plist);                  // 头删
bool Del_Tail(DLNode* plist);                  // 尾删
bool Del_Pos(DLNode* plist, int pos);          // 按位置删
bool Del_Val(DLNode* plist, ELEMTYPE val);     // 按值删(第一次)
bool Del_Val_All(DLNode* plist, ELEMTYPE val); // 按值删(全部)

四、核心函数

**插入核心:**先连新节点指针,再改旧节点指针,注意空链表边界。

**删除核心:**先断链,再释放内存,处理头尾节点特殊情况。

(一)插入函数

1. 头插
cpp 复制代码
bool Insert_Head(DLNode* plist, ELEMTYPE val) {
    assert(plist != nullptr);

    DLNode* pnewnode = new DLNode;
    pnewnode->data = val;

    pnewnode->next = plist->next;
    pnewnode->prior = plist;

    if (!IsEmpty(plist)) {
        plist->next->prior = pnewnode;
    }

    plist->next = pnewnode;
    return true;
}

**思路:**安全校验→申请新节点→按"新节点连前后→旧节点改指针"顺序操作,空链表无需修改原首节点前驱。

**易错点:**指针修改顺序不能乱,空链表需跳过原首节点前驱修改步骤。

2. 尾插
cpp 复制代码
bool Insert_Tail(DLNode* plist, ELEMTYPE val) {
    assert(plist != nullptr);

    DLNode* pnewnode = new DLNode;
    pnewnode->data = val;

    DLNode* p = plist;
    while (p->next != nullptr) {
        p = p->next;
    }

    pnewnode->next = p->next;
    pnewnode->prior = p;
    p->next = pnewnode;

    return true;
}

**思路:**遍历找到尾节点→新节点与尾节点双向关联,无需判断空链表(空链表时p指向头结点,逻辑仍成立)。

**说明:**尾插是按位置插的特例(pos=链表长度)。

3. 按位置插(核心)
cpp 复制代码
bool Insert_Pos(DLNode* plist, ELEMTYPE val, int pos) {
    assert(plist != nullptr);
    int len = Get_Length(plist);
    assert(pos >= 0 && pos <= len);

    if (pos == 0) return Insert_Head(plist, val);
    if (pos == len) return Insert_Tail(plist, val);

    DLNode* pnewnode = new DLNode;
    pnewnode->data = val;

    DLNode* p = plist;
    for (int i = 0; i < pos; i++) {
        p = p->next;
    }

    pnewnode->next = p->next;
    pnewnode->prior = p;
    p->next->prior = pnewnode;
    p->next = pnewnode;

    return true;
}
思路

1. 安全校验: 除了断言plist不为空,还要判断pos的合法性 ------pos不能小于 0,也不能大于链表长度(插入位置可以是 0~len,0 是头插,len 是尾插)。

2. 特例复用: 如果是头插或尾插,直接调用前面写好的Insert_HeadInsert_Tail,减少代码冗余,避免重复出错。

3. 通用插入逻辑(中间位置插入)

  • 先找到插入位置的前一个节点p(遍历 pos 次,从头部开始)
  • 按照 "新节点先连前后,再改原有节点" 的顺序修改指针,和头插的指针修改逻辑完全一致

**4. 本质:**头插(pos=0)、尾插(pos=len)是按位置插入的两个特殊情况,中间位置插入是通用情况,掌握通用逻辑,就能应对所有插入场景。

重点

**1. 指针挂接逻辑:**和头插完全一致,记住 "先连新节点的 next 和 prior,再修改插入位置前后节点的指针",避免指针丢失。

2. 合法性判断: pos的范围必须是0<=pos<=len,如果pos>len,插入位置超出链表范围,会导致操作空指针;如果pos<0,插入位置非法,直接断言报错。

【图片标注】此处可插入 "按位置插入(中间位置)示意图",标注插入位置、p指针的位置、指针修改的顺序,清晰展示通用插入逻辑。

(二)删除函数

1. 头删
cpp 复制代码
bool Del_Head(DLNode* plist) {
    assert(plist != nullptr);
    if (IsEmpty(plist)) return false;

    DLNode* p = plist->next;
    plist->next = p->next;

    if (p->next != nullptr) {
        p->next->prior = plist;
    }

    delete p;
    return true;
}

**思路:**判空→定位首节点→断链→释放内存,删除后有节点则修改新首节点前驱。

**易错点:**空链表不能删,删除最后一个节点无需修改新首节点前驱。

2. 尾删
cpp 复制代码
bool Del_Tail(DLNode* plist) {
    assert(plist != nullptr);
    if (IsEmpty(plist)) return false;

    DLNode* p = plist;
    while (p->next != nullptr) {
        p = p->next;
    }

    DLNode* q = p->prior;
    q->next = p->next;
    delete p;

    return true;
}

**思路:**判空→找到尾节点及其前驱→断链→释放尾节点,逻辑统一无需额外边界判断。

3. 按位置删(核心)
cpp 复制代码
bool Del_Pos(DLNode* plist, int pos) {
    assert(plist != nullptr);
    int len = Get_Length(plist);
    assert(pos >= 0 && pos < len);
    if (IsEmpty(plist)) return false;

    if (pos == 0) return Del_Head(plist);
    if (pos == len - 1) return Del_Tail(plist);

    DLNode* p = plist;
    for (int i = 0; i < pos; i++) {
        p = p->next;
    }

    DLNode* q = p->prior;
    q->next = p->next;
    p->next->prior = q;

    delete p;
    return true;
}
思路

1. 安全校验: pos的合法性的判断和插入不同 ------ 删除时pos不能大于等于链表长度(有效节点数为 len,pos 范围是 0~len-1),否则删除位置非法。

2. 特例复用: pos=0(头删)、pos=len-1(尾删),直接调用Del_HeadDel_Tail,减少代码冗余。

3. 中间位置删除

  • 遍历找到待删除节点p(pos 次遍历),用q指向p的前驱节点
  • 断链操作:让qnext跳过p,指向p的下一个节点;同时让p的下一个节点的prior指向q,彻底断开p与链表的关联

4. 释放内存: delete p并赋值 NULL,避免野指针。

关键点

1. pos合法性: 删除时pos不能等于 len(插入时可以等于 len),否则会操作空指针;

2. 断链顺序: 先修改前驱节点的next,再修改后继节点的prior,顺序不影响,但必须都修改,否则链表双向关联断裂。

4. 按值删(第一次出现)
cpp 复制代码
bool Del_Val(DLNode* plist, ELEMTYPE val) {
    assert(plist != nullptr);
    if (IsEmpty(plist)) return false;

    DLNode* p = Search(plist, val);
    if (p == nullptr) return false;

    DLNode* q = p->prior;
    q->next = p->next;

    if (p->next != nullptr) {
        p->next->prior = q;
    }

    delete p;
    return true;
}

**思路:**借助Search函数定位节点→断链(处理尾节点特例)→释放内存,未找到则返回false。

5. 按值删(全部)
cpp 复制代码
bool Del_Val_All(DLNode* plist, ELEMTYPE val) {
    assert(plist != nullptr);
    if (IsEmpty(plist)) return false;

    while (Search(plist, val) != nullptr) {
        DLNode* p = Search(plist, val);
        DLNode* q = p->prior;

        q->next = p->next;
        if (p->next != nullptr) {
            p->next->prior = q;
        }
        delete p;
    }
    return true;
}

**思路:**循环调用Search查找节点,找到则删除,直至无匹配节点,避免死循环。

五、工具函数

1. 初始化
复制代码
void Init_DoubleList(DLNode* plist) {
 assert(plist != nullptr);
 plist->next = nullptr;
 plist->prior = nullptr; 
}

**思路:**头结点前后驱置空,使链表处于空状态,避免空指针操作。

2. 判空
复制代码
bool IsEmpty(DLNode* plist) {
 assert(plist != nullptr);
 return plist->next == nullptr; 
}

**思路:**头结点next为空,即无有效节点,判空逻辑简洁高效。

3. 获取有效长度
复制代码
int Get_Length(DLNode* plist) {
 assert(plist != nullptr);
 int count = 0;
 for (DLNode* p = plist->next; p != nullptr; p = p->next) {
  count++;
 }
 return count;
}

**思路:**遍历有效节点计数,不统计头结点,遍历至空节点停止。

4. 清空
复制代码
bool Clear(DLNode* plist) {
 assert(plist != nullptr);
 Destroy2(plist);
 return true; 
}

**思路:**复用Destroy2逻辑,删除所有有效节点,保留头结点,回归初始化状态。

5. 销毁1(循环头删)
复制代码
bool Destroy1(DLNode* plist) {
 assert(plist != nullptr);
 while (!IsEmpty(plist)) {
  Del_Head(plist);
 }
 return true; 
}

**思路:**循环头删,直至链表为空,实现简单,适合新手。

6. 销毁2(双指针配合)
复制代码
bool Destroy2(DLNode* plist) {
 assert(plist != nullptr);
 DLNode* p = plist->next;
 DLNode* q = nullptr;
 while (p != nullptr) {
  q = p->next;
  delete p;
  p = q;
 }
 plist->next = nullptr;
 return true; 
}

**思路:**双指针遍历,先记录下一个节点,再释放当前节点,效率高于Destroy1。

7. 打印
复制代码
bool Show(DLNode* plist) {
 assert(plist != nullptr);
 for (DLNode* p = plist->next; p != nullptr; p = p->next) {
  printf("%d ", p->data);
 }
 printf("\n");
 return true; 
}

**思路:**正向遍历有效节点并打印,换行提升可读性,方便调试。

8. 查找
复制代码
DLNode* Search(DLNode* plist, ELEMTYPE val) {
 assert(plist != nullptr);
 DLNode* p = plist->next; while (p != nullptr) {
  if (p->data == val) {
   return p;
  }
  p = p->next;
 }
 return nullptr; 
}

**思路:**正向遍历,找到第一个匹配节点返回指针,未找到返回nullptr,供删除函数使用。

六、总结

  1. 双向链表核心:前驱指针(prior)+ 后继指针(next),解决单链表痛点,支持双向遍历。

  2. 插入关键:指针挂接顺序(先连新节点)、空链表/头尾边界判断,避免指针丢失。

  3. 删除关键:先断链、再释放内存,处理尾节点等边界,避免内存泄漏。

  4. 核心关联:头插/尾插是按位置插的特例,头删/尾删是按位置删的特例,掌握通用逻辑即可应对所有场景。

  5. 新手避坑:做好断言和判空,指针修改顺序不能乱,删除后及时释放内存。

相关推荐
py有趣2 小时前
力扣热门100题之编辑距离
数据结构·算法·leetcode
努力努力再努力wz3 小时前
【Linux网络系列】万字硬核解析网络层核心:IP协议到IP 分片重组、NAT技术及 RIP/OSPF 动态路由全景
java·linux·运维·服务器·数据结构·c++·python
谭欣辰4 小时前
AC自动机:多模式匹配的高效利器
数据结构·c++·算法
历程里程碑5 小时前
MySQL事务深度解析:ACID到MVCC实战+万字长文解析
开发语言·数据结构·数据库·c++·sql·mysql·排序算法
qiqsevenqiqiqiqi5 小时前
MC0550鱼肠剑试锋芒
数据结构·算法
仍然.5 小时前
算法题目---链表
数据结构·算法·链表
周末也要写八哥7 小时前
最长递增子序列典型应用题目详解
数据结构·算法
iiiiyu7 小时前
常用API(StringJoiner类 & Math类 & System类)
java·大数据·开发语言·数据结构·编程语言
小糯米6017 小时前
C语言指针3
c语言·数据结构·算法