C语言——双向链表

前言

双向链表是线性表的重要链式存储结构,相比单链表,其每个节点增加了前驱指针,支持双向遍历和已知节点的O(1)删除,在浏览器前进后退、LRU缓存、双向遍历场景中应用广泛。本文从结构定义、核心操作、完整代码、特性分析、应用场景五个维度,全面讲解双向链表的知识点,所有代码均附带详细注释,可直接编译运行。


一、双向链表的核心结构

1. 节点结构

双向链表的每个节点包含前驱指针(prev)、数据域(data)、后继指针(next) 三部分:

• prev:指向当前节点的上一个节点,头节点prev为NULL

• data:存储节点的有效数据

• next:指向当前节点的下一个节点,尾节点next为NULL

2. 结构体定义(C/C++)

本文基于带头节点的双向链表实现(带头节点可统一空链表和非空链表的操作,减少边界判断冗余),基础int类型节点定义如下:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

// 定义双向链表节点结构体
typedef struct DNode {
    int data;           // 数据域:存储节点数据
    struct DNode* prev; // 前驱指针:指向当前节点的上一个节点
    struct DNode* next; // 后继指针:指向当前节点的下一个节点
} DNode, *DLinkList;    // DNode为节点类型,DLinkList为节点指针类型(链表)

3. 逻辑结构

• 空链表:头节点->prev=NULL + 头节点->next=NULL

• 非空链表:NULL <- 节点1 <-> 节点2 <-> 节点3 -> NULL

二、双向链表的核心操作(全代码+详细注释)

双向链表操作的核心原则:先处理后继指针,再处理前驱指针(避免指针丢失),所有操作均围绕带头节点实现,以下为全量核心操作代码。

  1. 初始化链表

创建头节点,初始化其前驱和后继指针为NULL,返回初始化结果。

cpp 复制代码
// 初始化双向链表(带头节点)
bool InitDLinkList(DLinkList *L) {
    *L = (DNode*)malloc(sizeof(DNode)); // 为头节点分配内存
    if (*L == NULL) { // 内存分配失败(如内存溢出),初始化失败
        return false;
    }
    (*L)->prev = NULL; // 头节点无前驱,prev置空
    (*L)->next = NULL; // 空链表,头节点无后继,next置空
    return true;
}
  1. 判断链表是否为空

利用带头节点特性,仅需判断头节点的后继指针是否为NULL。

cpp 复制代码
// 判断双向链表是否为空
bool IsEmpty(DLinkList L) {
    return L->next == NULL; // 空链表返回true,非空返回false
}
  1. 销毁链表

从头节点开始,依次释放所有节点内存,最终将头指针置空,避免野指针和内存泄漏。

cpp 复制代码
// 销毁双向链表(释放所有节点内存,包括头节点)
bool DestroyDLinkList(DLinkList *L) {
    DNode *p = *L;    // p指向当前要释放的节点,初始为头节点
    DNode *q = NULL;  // q保存p的后继节点(防止释放后找不到下一个节点)
    while (p != NULL) {
        q = p->next;  // 先保存后继节点地址
        free(p);      // 释放当前节点内存
        p = q;        // p移动到下一个节点
    }
    *L = NULL;        // 头指针置空,避免野指针
    return true;
}
  1. 清空链表

释放所有数据节点,保留头节点,后续可继续使用链表,与销毁的核心区别是保留头节点。

cpp 复制代码
// 清空双向链表(保留头节点,删除所有数据节点)
bool ClearDLinkList(DLinkList L) {
    DNode *p = L->next; // p指向第一个数据节点
    DNode *q = NULL;    // 保存p的后继节点
    while (p != NULL) {
        q = p->next;
        free(p);
        p = q;
    }
    L->next = NULL;     // 头节点后继置空,恢复空链表状态
    return true;
}
  1. 获取链表长度

从第一个数据节点开始正序遍历,统计数据节点个数(头节点不计入长度)。

cpp 复制代码
// 获取双向链表长度(仅统计数据节点,头节点不计)
int GetLength(DLinkList L) {
    int len = 0;
    DNode *p = L->next; // p指向第一个数据节点
    while (p != NULL) { // 遍历至尾节点结束
        len++;
        p = p->next;
    }
    return len;
}
  1. 按位置查找节点

找到第i个数据节点,返回其指针;位置非法时返回NULL(i从1开始)。

cpp 复制代码
// 按位置查找:获取第i个数据节点的指针(i从1开始)
// 成功返回节点指针,失败(位置非法/空链表)返回NULL
DNode* GetElem(DLinkList L, int i) {
    if (i < 1 || IsEmpty(L)) { // 位置小于1或空链表,直接返回NULL
        return NULL;
    }
    int j = 1;
    DNode *p = L->next; // p指向第一个数据节点
    while (p != NULL && j < i) { // 未到第i个节点且未遍历完
        p = p->next;
        j++;
    }
    return p; // 遍历结束后,p为NULL则位置非法,否则为目标节点
}
  1. 按值查找节点

正序遍历链表,找到首个数据域等于e的节点,返回其指针;无匹配节点返回NULL。

cpp 复制代码
// 按值查找:获取首个数据域为e的节点指针
DNode* LocateElem(DLinkList L, int e) {
    if (IsEmpty(L)) { // 空链表无匹配节点
        return NULL;
    }
    DNode *p = L->next;
    while (p != NULL) {
        if (p->data == e) { // 找到匹配节点,直接返回
            return p;
        }
        p = p->next;
    }
    return NULL; // 遍历结束无匹配
}
  1. 插入节点(第i个节点前插入)

双向链表插入核心步骤:先连新节点的后继,再连前驱,避免指针丢失。

cpp 复制代码
// 插入节点:在第i个数据节点**之前**插入数据为e的新节点
bool ListInsert(DLinkList L, int i, int e) {
    if (i < 1) { // 插入位置小于1,非法
        return false;
    }
    DNode *p = GetElem(L, i-1); // 找到第i-1个节点(插入位置的前驱)
    if (p == NULL) { // 第i-1个节点不存在(i超过长度+1),插入失败
        return false;
    }
    // 1. 创建新节点并初始化数据域
    DNode *s = (DNode*)malloc(sizeof(DNode));
    if (s == NULL) { // 内存分配失败,插入失败
        return false;
    }
    s->data = e;
    // 2. 先连新节点的后继,再连前驱(核心顺序,防止指针丢失)
    s->next = p->next;  // 新节点的后继 = 前驱节点的原后继
    if (p->next != NULL) { // 若前驱不是尾节点,修改原后继的前驱为新节点
        p->next->prev = s;
    }
    p->next = s;        // 前驱节点的后继 = 新节点
    s->prev = p;        // 新节点的前驱 = 前驱节点
    return true;
}
  1. 按位置删除节点

删除核心步骤:先修改前驱的后继,再修改后继的前驱,最后释放节点内存。

cpp 复制代码
// 删除节点:删除第i个数据节点
bool ListDelete(DLinkList L, int i) {
    if (i < 1 || IsEmpty(L)) { // 位置非法或空链表,删除失败
        return false;
    }
    DNode *q = GetElem(L, i); // 找到第i个待删除节点
    if (q == NULL) { // 待删除节点不存在,删除失败
        return false;
    }
    // 1. 待删除节点的前驱 指向 待删除节点的后继
    q->prev->next = q->next;
    // 2. 若待删除节点不是尾节点,其后继 指向 待删除节点的前驱
    if (q->next != NULL) {
        q->next->prev = q->prev;
    }
    // 3. 释放待删除节点内存
    free(q);
    return true;
}
  1. 已知节点的O(1)删除(双向链表独有优势)

单链表已知节点删除需O(n)遍历找前驱,双向链表可通过prev直接获取前驱,实现O(1)时间复杂度删除。

cpp 复制代码
// 已知节点p,O(1)时间删除该节点(双向链表独有优势)
// 注意:p必须是链表中存在的有效节点
bool DeleteNode(DLinkList L, DNode *p) {
    if (p == NULL || L == NULL) { // 节点或链表为空,删除失败
        return false;
    }
    // 1. 前驱节点的后继 指向 待删除节点的后继
    p->prev->next = p->next;
    // 2. 若待删除节点不是尾节点,后继节点的前驱 指向前驱节点
    if (p->next != NULL) {
        p->next->prev = p->prev;
    }
    // 3. 释放节点内存
    free(p);
    return true;
}
  1. 双向遍历(正序+逆序)

双向链表的核心优势之一,支持从头到尾和从尾到头两种遍历方式,逆序遍历需先找到尾节点。

cpp 复制代码
// 正序遍历双向链表:从第一个数据节点到尾节点
void TraverseForward(DLinkList L) {
    if (IsEmpty(L)) {
        printf("链表为空,无数据可遍历!\n");
        return;
    }
    DNode *p = L->next;
    printf("正序遍历:");
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

// 逆序遍历双向链表:从尾节点到第一个数据节点(双向链表独有)
void TraverseBackward(DLinkList L) {
    if (IsEmpty(L)) {
        printf("链表为空,无数据可遍历!\n");
        return;
    }
    DNode *p = L->next;
    // 先找到尾节点(next为NULL的节点)
    while (p->next != NULL) {
        p = p->next;
    }
    printf("逆序遍历:");
    // 从尾节点向前遍历,至头节点停止
    while (p != L) {
        printf("%d ", p->data);
        p = p->prev;
    }
    printf("\n");
}
  1. 链表创建(尾插法+头插法)

尾插法:按数据顺序创建,适合常规存储(先进先出)

cpp 复制代码
// 尾插法创建双向链表:arr为数据数组,n为数组长度
bool CreateDLinkList_Tail(DLinkList *L, int arr[], int n) {
    if (!InitDLinkList(L)) { // 先初始化链表
        return false;
    }
    DNode *tail = *L; // 尾指针,初始指向头节点,始终指向尾节点
    for (int i = 0; i < n; i++) {
        // 创建新节点
        DNode *s = (DNode*)malloc(sizeof(DNode));
        if (s == NULL) {
            return false;
        }
        s->data = arr[i];
        // 插入到尾节点后
        s->next = tail->next;
        s->prev = tail;
        tail->next = s;
        tail = s; // 尾指针后移,指向新的尾节点
    }
    return true;
}
头插法:创建后数据逆序,适合实现栈(后进先出)
// 头插法创建双向链表:arr为数据数组,n为数组长度
bool CreateDLinkList_Head(DLinkList *L, int arr[], int n) {
    if (!InitDLinkList(L)) { // 先初始化链表
        return false;
    }
    for (int i = 0; i < n; i++) {
        DNode *s = (DNode*)malloc(sizeof(DNode));
        if (s == NULL) {
            return false;
        }
        s->data = arr[i];
        // 插入到头节点后,与普通插入逻辑一致
        s->next = (*L)->next;
        if ((*L)->next != NULL) {
            (*L)->next->prev = s;
        }
        (*L)->next = s;
        s->prev = *L;
    }
    return true;
}

三、完整测试代码(可直接编译运行)

将上述所有操作整合,测试功能正确性,可直接复制到VS Code、Dev-C++、Clion等编译器运行。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

// 定义双向链表节点结构体
typedef struct DNode {
    int data;
    struct DNode* prev;
    struct DNode* next;
} DNode, *DLinkList;

// 初始化双向链表(带头节点)
bool InitDLinkList(DLinkList *L) {
    *L = (DNode*)malloc(sizeof(DNode));
    if (*L == NULL) {
        return false;
    }
    (*L)->prev = NULL;
    (*L)->next = NULL;
    return true;
}

// 判断双向链表是否为空
bool IsEmpty(DLinkList L) {
    return L->next == NULL;
}

// 销毁双向链表(释放所有节点内存,包括头节点)
bool DestroyDLinkList(DLinkList *L) {
    DNode *p = *L;
    DNode *q = NULL;
    while (p != NULL) {
        q = p->next;
        free(p);
        p = q;
    }
    *L = NULL;
    return true;
}

// 清空双向链表(保留头节点,删除所有数据节点)
bool ClearDLinkList(DLinkList L) {
    DNode *p = L->next;
    DNode *q = NULL;
    while (p != NULL) {
        q = p->next;
        free(p);
        p = q;
    }
    L->next = NULL;
    return true;
}

// 获取双向链表长度(仅统计数据节点,头节点不计)
int GetLength(DLinkList L) {
    int len = 0;
    DNode *p = L->next;
    while (p != NULL) {
        len++;
        p = p->next;
    }
    return len;
}

// 按位置查找:获取第i个数据节点的指针(i从1开始)
DNode* GetElem(DLinkList L, int i) {
    if (i < 1 || IsEmpty(L)) {
        return NULL;
    }
    int j = 1;
    DNode *p = L->next;
    while (p != NULL && j < i) {
        p = p->next;
        j++;
    }
    return p;
}

// 按值查找:获取首个数据域为e的节点指针
DNode* LocateElem(DLinkList L, int e) {
    if (IsEmpty(L)) {
        return NULL;
    }
    DNode *p = L->next;
    while (p != NULL) {
        if (p->data == e) {
            return p;
        }
        p = p->next;
    }
    return NULL;
}

// 插入节点:在第i个数据节点**之前**插入数据为e的新节点
bool ListInsert(DLinkList L, int i, int e) {
    if (i < 1) {
        return false;
    }
    DNode *p = GetElem(L, i-1);
    if (p == NULL) {
        return false;
    }
    DNode *s = (DNode*)malloc(sizeof(DNode));
    if (s == NULL) {
        return false;
    }
    s->data = e;
    s->next = p->next;
    if (p->next != NULL) {
        p->next->prev = s;
    }
    p->next = s;
    s->prev = p;
    return true;
}

// 删除节点:删除第i个数据节点
bool ListDelete(DLinkList L, int i) {
    if (i < 1 || IsEmpty(L)) {
        return false;
    }
    DNode *q = GetElem(L, i);
    if (q == NULL) {
        return false;
    }
    q->prev->next = q->next;
    if (q->next != NULL) {
        q->next->prev = q->prev;
    }
    free(q);
    return true;
}

// 已知节点p,O(1)时间删除该节点
bool DeleteNode(DLinkList L, DNode *p) {
    if (p == NULL || L == NULL) {
        return false;
    }
    p->prev->next = p->next;
    if (p->next != NULL) {
        p->next->prev = p->prev;
    }
    free(p);
    return true;
}

// 正序遍历双向链表
void TraverseForward(DLinkList L) {
    if (IsEmpty(L)) {
        printf("链表为空,无数据可遍历!\n");
        return;
    }
    DNode *p = L->next;
    printf("正序遍历:");
    while (p != NULL) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

// 逆序遍历双向链表
void TraverseBackward(DLinkList L) {
    if (IsEmpty(L)) {
        printf("链表为空,无数据可遍历!\n");
        return;
    }
    DNode *p = L->next;
    while (p->next != NULL) {
        p = p->next;
    }
    printf("逆序遍历:");
    while (p != L) {
        printf("%d ", p->data);
        p = p->prev;
    }
    printf("\n");
}

// 尾插法创建双向链表
bool CreateDLinkList_Tail(DLinkList *L, int arr[], int n) {
    if (!InitDLinkList(L)) {
        return false;
    }
    DNode *tail = *L;
    for (int i = 0; i < n; i++) {
        DNode *s = (DNode*)malloc(sizeof(DNode));
        if (s == NULL) {
            return false;
        }
        s->data = arr[i];
        s->next = tail->next;
        s->prev = tail;
        tail->next = s;
        tail = s;
    }
    return true;
}

// 头插法创建双向链表
bool CreateDLinkList_Head(DLinkList *L, int arr[], int n) {
    if (!InitDLinkList(L)) {
        return false;
    }
    for (int i = 0; i < n; i++) {
        DNode *s = (DNode*)malloc(sizeof(DNode));
        if (s == NULL) {
            return false;
        }
        s->data = arr[i];
        s->next = (*L)->next;
        if ((*L)->next != NULL) {
            (*L)->next->prev = s;
        }
        (*L)->next = s;
        s->prev = *L;
    }
    return true;
}

// 主函数:测试所有操作
int main() {
    DLinkList L;
    int arr[] = {1, 2, 3, 4, 5};
    int n = sizeof(arr) / sizeof(arr[0]);

    // 1. 尾插法创建链表
    if (CreateDLinkList_Tail(&L, arr, n)) {
        printf("===== 尾插法创建链表成功 =====\n");
        TraverseForward(L); // 正序:1 2 3 4 5
        TraverseBackward(L);// 逆序:5 4 3 2 1
        printf("链表长度:%d\n\n", GetLength(L)); // 5
    }

    // 2. 插入节点:在第3个节点前插入6
    if (ListInsert(L, 3, 6)) {
        printf("===== 在第3个节点前插入6 =====\n");
        TraverseForward(L); // 1 2 6 3 4 5
        printf("\n");
    }

    // 3. 删除节点:删除第6个节点
    if (ListDelete(L, 6)) {
        printf("===== 删除第6个节点 =====\n");
        TraverseForward(L); // 1 2 6 3 4
        printf("\n");
    }

    // 4. 按值查找+已知节点删除
    DNode *p = LocateElem(L, 6);
    if (p != NULL) {
        printf("===== 找到值为6的节点,执行O(1)删除 =====\n");
        DeleteNode(L, p);
        TraverseForward(L); // 1 2 3 4
        printf("\n");
    }

    // 5. 清空链表
    if (ClearDLinkList(L)) {
        printf("===== 清空链表 =====\n");
        printf("链表是否为空:%s\n\n", IsEmpty(L) ? "是" : "否"); // 是
    }

    // 6. 销毁链表
    if (DestroyDLinkList(&L)) {
        printf("===== 销毁链表 =====\n");
        printf("链表销毁成功!\n");
    }

    return 0;
}

运行结果

cpp 复制代码
===== 尾插法创建链表成功 =====
正序遍历:1 2 3 4 5 
逆序遍历:5 4 3 2 1 
链表长度:5

===== 在第3个节点前插入6 =====
正序遍历:1 2 6 3 4 5 

===== 删除第6个节点 =====
正序遍历:1 2 6 3 4 

===== 找到值为6的节点,执行O(1)删除 =====
正序遍历:1 2 3 4 

===== 清空链表 =====
链表是否为空:是

===== 销毁链表 =====
链表销毁成功!

四、核心特性

时间复杂度

• O(1):初始化/判空/销毁(已知节点)、已知节点的插入/删除

• O(n):按位置插删、按值/位置查找、正/逆序遍历、头/尾插法创建

空间复杂度

• 单操作:O(1)(仅常数个辅助指针)

• 链表创建:O(n)(存储节点数据与双指针域)

优点

  1. 支持正/逆序双向遍历,灵活性远超单链表

  2. 已知节点时O(1)删除,解决单链表性能瓶颈

  3. 带头节点实现,统一空/非空链表操作,减少边界判断

  4. 前驱/后继指向明确,头/尾节点边界处理更友好

缺点

  1. 每个节点多一个前驱指针,空间开销大、存储密度低

  2. 插删需同时维护prev/next,操作繁琐易出指针错误

  3. 节点内存占用稍大,存在轻微内存分配失败风险

五、与单链表核心对比

|--------|----------------|------------|
| 特性 | 双向链表 | 单链表 |
| 节点结构 | prev+data+next | data+next |
| 遍历方式 | 正/逆序双向遍历 | 仅正序遍历 |
| 已知节点删除 | O(1)(核心优势) | O(n)(需找前驱) |
| 空间开销 | 较大(双指针) | 较小(单指针) |
| 操作复杂度 | 稍高(维护双指针) | 较低(维护单指针) |
| 存储密度 | 较低 | 较高 |
| 核心优势 | 双向遍历、O(1)删已知节点 | 实现简单、空间开销小 |

六、典型应用场景

适配双向遍历或频繁删除已知节点的需求:

  1. 浏览器前进/后退、文件管理器上下级切换

  2. LRU缓存淘汰算法(快速删除最久未使用节点)

  3. 聊天记录上下翻页、有序链表动态维护

七、高频错误与避坑指南

问题多集中在指针维护和边界判断,对应解决方案:

  1. 插删指针顺序错误→ 插入遵循先后继、后前驱原则

  2. 删除尾节点越界→ 操作前判断node->next != NULL

  3. 逆序遍历终止错误→ 以p != 头节点为终止条件,避免访问NULL

  4. 野指针问题→ 释放节点/销毁链表后,及时将相关指针置NULL

  5. 内存泄漏→ 所有malloc后必须检查返回值是否为NULL

  6. 混淆头/数据节点→ 遍历从头节点->next开始,头节点仅作哨兵

八、总结

双向链表通过增加前驱指针,以少量空间开销,换取双向遍历和O(1)删除已知节点的核心优势,是单链表的优化版本。

其学习核心为:理解指针指向关系,牢记插删的指针操作顺序,做好边界判断;所有操作本质都是对prev和next指针的合理维护,抓住指针即掌握双向链表核心。掌握后可拓展学习循环双向链表、LRU缓存等进阶应用,落地实际开发。

相关推荐
轩情吖2 小时前
数据结构-图
数据结构·c++·邻接表·邻接矩阵·最小生成树·kruskal算法·prim算法
Wh-Constelltion2 小时前
【PQ分解法潮流计算(matlab版)】
算法·matlab
Prince-Peng2 小时前
技术架构系列 - 详解Redis
数据结构·数据库·redis·分布式·缓存·中间件·架构
lxl13073 小时前
学习C++(5)运算符重载+赋值运算符重载
学习
zhuqiyua3 小时前
第一次课程家庭作业
c++
只是懒得想了3 小时前
C++实现密码破解工具:从MD5暴力破解到现代哈希安全实践
c++·算法·安全·哈希算法
ruxshui3 小时前
个人笔记: 星环Inceptor/hive普通分区表与范围分区表核心技术总结
hive·hadoop·笔记
码农水水3 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
慾玄3 小时前
渗透笔记总结
笔记