跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C算法

数据结构:双向链表详解(结构、实现与算法实战)

详细讲解了带头双向循环链表的概念、结构及核心实现。内容涵盖初始化、头尾插删、销毁、查找等基础接口,并通过代码解析了指针操作的细节与注意事项。此外,对比了顺序表与链表在存储与操作上的差异,并结合移除元素、反转链表等经典算法题,展示了链表在实际编程中的应用技巧与优化思路。

追风少年发布于 2026/3/23更新于 2026/10/272 浏览
数据结构:双向链表详解(结构、实现与算法实战)

双向链表概念与结构

带头双向循环链表

双向链表是一种链式存储的数据结构,每个节点包含两个指针:一个指向前驱节点(prev),一个指向后继节点(next),同时包含数据域(data)。这种结构允许双向遍历,支持更灵活的插入和删除操作,但相比单链表会增加一定的空间开销。

我们通常使用的是带头双向循环链表。它兼具'头节点''双向指针''循环结构'三大特性。其中的头节点不存储有效数据,仅作为哨兵位,核心价值是简化边界操作(如插入/删除首节点时无需特殊判断)。

双向链表结构

基于单向不带头不循环链表的知识,我们可以定义如下结构体:

typedef int type;
typedef struct ListNode {
    type data;      // 数据域
    struct ListNode* prev; // 前驱指针
    struct ListNode* next; // 后继指针
} ListNode;

实现双向链表

1. 初始化

在双向链表中,头节点需要初始化。数据域可以存任意值,前驱和后继指针都指向自己即可形成空循环。

void LTInit(ListNode** h) {
    ListNode* ph = (ListNode*)malloc(sizeof(ListNode));
    if (ph == NULL) {
        perror("malloc fail!");
        exit(1);
    }
    *h = ph;
    (*h)->data = -1;
    (*h)->next = *h;
    (*h)->prev = *h;
}

这里使用二级指针 **h 是为了让函数内部能修改外部传入的头指针地址。初始化后,链表状态为 h <-> h。

2. 尾插与头插

尾插(LTPushBack)

对于带头节点的双向循环链表,尾插可直接通过头节点的 prev 指针定位尾节点,无需遍历,时间复杂度为 O(1)。

ListNode* LTcreat(type x) {
    ListNode* ph = (ListNode*)((ListNode));
     (ph == ) { perror(); (); }
    ph->data = x;
    ph->next = ph;
    ph->prev = ph;
     ph;
}

  {
    ListNode* p = LTcreat(x);
    p->next = h;
    p->prev = h->prev;
    h->prev->next = p;
    h->prev = p;
}
malloc
sizeof
if
NULL
"malloc fail!"
exit
1
return
void
LTPushBack
(ListNode* h, type x)

核心逻辑:新节点 p 的 next 指向头节点,prev 指向原尾节点;原尾节点的 next 指向 p;最后更新头节点的 prev 为 p。

头插(LTPushFront)

头插是将新节点插入到头节点之后、第一个有效节点之前。

void LTPushFront(ListNode* h, type x) {
    ListNode* p = LTcreat(x);
    p->next = h->next;
    p->prev = h;
    h->next->prev = p;
    h->next = p;
}

注意顺序:先保存原后继节点关系,再断链重连,避免丢失链表后半部分。

3. 删除操作

判空

在进行删除前,必须先检查链表是否为空。

bool LTEmpty(ListNode* phead) {
    assert(phead);
    return phead->next == phead;
}
尾删(LTPopBack)

删除最后一个有效节点。由于是循环链表,头节点的 prev 直接指向尾节点。

void LTPopBack(ListNode* h) {
    if (LTEmpty(h)) return;
    ListNode* p = h->prev;
    h->prev = p->prev;
    p->prev->next = h;
    free(p);
}

关键点:修改指针时务必保证循环不断裂。如果只改 h->prev 而不改 p->prev->next,会导致原尾节点的前驱仍指向被删节点,形成错误子链。

头删(LTPopFront)

删除头节点后的第一个有效节点。

void LTPopFront(ListNode* h) {
    if (LTEmpty(h)) { printf("链表为空,无法头删\n"); return; }
    ListNode* p = h->next;
    h->next = p->next;
    p->next->prev = h;
    free(p);
}

4. 销毁

销毁需遍历所有节点并逐个释放内存,最后释放头结点。

void LTDestory(ListNode* h) {
    if (LTEmpty(h)) { free(h); return; }
    ListNode* p = h->next;
    while (p != h) {
        ListNode* pr = p;
        p = p->next;
        free(pr);
    }
    free(h);
    h = NULL;
}

5. 查找与插入

查找
ListNode* LTFind(ListNode* h, type x) {
    if (LTEmpty(h)) return NULL;
    ListNode* p = h->next;
    while (p != h) {
        if (p->data == x) return p;
        p = p->next;
    }
    return NULL;
}
指定位置插入

在 pos 之后插入:

void LTInsert(ListNode* pos, type x) {
    assert(pos);
    ListNode* p = pos->next;
    ListNode* newnode = LTcreat(x);
    pos->next = newnode;
    newnode->prev = pos;
    newnode->next = p;
    p->prev = newnode;
}

在 pos 之前插入:

void LTInsertfront(ListNode* h, ListNode* pos, type x) {
    if (LTEmpty(h)) return;
    ListNode* p = h;
    while (p->next != h) {
        if (p->next == pos) break;
        p = p->next;
    }
    if (p->next == h) return;
    ListNode* newnode = LTcreat(x);
    ListNode* pr = p->next;
    newnode->next = pr;
    newnode->prev = p;
    p->next = newnode;
    pr->prev = newnode;
}
指定位置删除
void LTErase(ListNode* pos) {
    assert(pos);
    ListNode* p = pos->prev;
    p->next = pos->next;
    pos->next->prev = p;
    free(pos);
    pos = NULL;
}

6. 测试代码示例

为了验证功能,通常需要编写测试主程序。以下是一个整合了上述接口的测试流程:

#include"list.h"

void test() {
    ListNode* h;
    LTInit(&h);
    LTPushBack(h, 10);
    LTPushBack(h, 15);
    print(h);
    LTPushFront(h, 2);
    LTPushFront(h, 12);
    print(h);
    LTPopBack(h);
    LTPopFront(h);
    LTDestory(h);
}

int main() {
    test();
    return 0;
}

顺序表与链表的对比

特性顺序表链表
存储方式连续内存非连续内存
随机访问支持 (O(1))不支持 (O(n))
插入删除效率低 (需移动元素)效率高 (O(1),仅需改指针)
空间占用固定大小或扩容开销动态分配,有指针开销

结论:顺序表适合频繁随机访问、数据量固定的场景;链表适合频繁插入删除、数据量动态变化的场景。

链表算法实战

移除链表元素

题目要求移除值为 val 的节点。可以通过遍历原链表,构建一个新链表来存储符合要求的节点。

struct ListNode* removeElements(struct ListNode* head, int val) {
    struct ListNode *h = NULL, *pr = NULL;
    struct ListNode *p = head;
    while (p) {
        if (p->val != val) {
            if (h == NULL) {
                h = p; pr = p;
            } else {
                pr->next = p; pr = p;
            }
        }
        p = p->next;
    }
    if (pr) pr->next = NULL;
    return h;
}

思路是维护新链表的头 h 和尾 pr,遇到不匹配值的节点就拼接到新链表尾部,最后记得将尾节点的 next 置空。

反转链表

反转链表推荐使用迭代法,通过三个指针逐次反转节点指向。

struct ListNode* reverseList(struct ListNode* head) {
    struct ListNode *s1 = NULL, *s2 = head, *s3 = NULL;
    if (s2) s3 = s2->next;
    while (s2) {
        s2->next = s1;
        s1 = s2;
        s2 = s3;
        if (s3) s3 = s3->next;
    }
    return s1;
}
  • s1:已反转部分的头节点。
  • s2:当前待反转节点。
  • s3:临时保存 s2 的下一个节点,防止断链。

每次循环将 s2 的 next 指向 s1,然后三个指针依次向后移动,直到 s2 为空。

目录

  1. 双向链表概念与结构
  2. 带头双向循环链表
  3. 双向链表结构
  4. 实现双向链表
  5. 1. 初始化
  6. 2. 尾插与头插
  7. 尾插(LTPushBack)
  8. 头插(LTPushFront)
  9. 3. 删除操作
  10. 判空
  11. 尾删(LTPopBack)
  12. 头删(LTPopFront)
  13. 4. 销毁
  14. 5. 查找与插入
  15. 查找
  16. 指定位置插入
  17. 指定位置删除
  18. 6. 测试代码示例
  19. 顺序表与链表的对比
  20. 链表算法实战
  21. 移除链表元素
  22. 反转链表

更多推荐文章

查看全部
  • 开源AI智能名片:S2B2C商城的链动2+1裂变实战
  • 2023 年全国职业院校技能大赛网络建设与运维赛项样题解析
  • DeepSeek 各版本详解与优缺点对比
  • 三维人体姿态估计前沿算法与论文案例
  • C++11 关键新特性回顾与实践
  • QQ 机器人接入 OpenClaw 的部署记录
  • OpenClaw Web Search 配置与渠道选择指南
  • 基于 Leaflet Trackplayer 的 WebGIS 高速轨迹可视化实战
  • Angular 版本升级全流程指南与核心注意事项
  • Python 基于 Playwright 的自动化环境配置指南(Windows 与 Linux)
  • Vue3 + PlayCanvas 实战:3D 地图自由巡视闯关游戏开发
  • JSON 技术详解:从诞生历史到核心优势
  • 2026免费AI网文写作工具实测:六款用法与避坑心得
  • Java SE 文件 IO 基础:File 类与文件系统操作
  • SpringBoot 整合 Neo4j 图数据库实战
  • AI 大模型收费指标 Token 详解
  • Mac mini 部署 OpenClaw:接入国产大模型与飞书机器人
  • AI 时代产品经理成长指南
  • OpenClaw 小白入门:定位、部署与核心场景
  • Topaz Video AI 视频画质增强与修复软件安装指南

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online