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

数据结构:双向链表原理与 C 语言实现

详细讲解了数据结构中的双向链表,包括其带头结点的循环结构设计、C 语言实现细节及核心 API。内容涵盖节点申请、初始化、销毁、头尾插删、指定位置插入删除以及查找功能。重点分析了双向链表与顺序表在存储空间、访问效率及增删性能上的差异,帮助读者理解不同线性表的适用场景。代码部分经过优化,注释清晰,便于实战参考。

ApiHolic发布于 2026/3/29更新于 2026/10/2689 浏览
数据结构:双向链表原理与 C 语言实现

数据结构:双向链表

在数据结构中,双向链表是基础且重要的线性表结构。相比单链表,它在每个节点中增加了前驱指针,使得遍历更加灵活,支持双向查找。

概念与结构

核心概念

双向链表通常采用带头结点的循环结构。这里的'头结点'是一个哨兵位,不存储有效数据,仅用于简化边界条件的处理(如空表判断、首尾插入)。

节点定义

// 定义数据类型
typedef int LTDataType;

// 定义双向链表节点结构
typedef struct ListNode {
    LTDataType data;
    struct ListNode* next; // 指向下一个节点
    struct ListNode* prev; // 指向前一个节点
} LTNode;

接口设计

为了保持接口的统一性和易用性,我们约定所有操作函数都接收头指针 LTNode* phead。初始化函数直接返回头指针,销毁函数也返回 NULL 以便接收者重置指针。

// 初始化
LTNode* LTInit();

// 销毁
LTNode* LTDestroy(LTNode* phead);

// 增删改查
void LTPushBack(LTNode* phead, LTDataType x); // 尾插
void LTPushFront(LTNode* phead, LTDataType x); // 头插
void LTPopBack(LTNode* phead);                // 尾删
void LTPopFront(LTNode* phead);                // 头删
bool LTEmpty(LTNode* phead);                   // 判空
void LTPrint(LTNode* phead);                   // 打印
LTNode* LTFind(LTNode* phead, LTDataType x);   // 查找
void LTInsert(LTNode* pos, LTDataType x);      // pos 后插入
void LTErase(LTNode* pos);                     // 删除 pos 节点

核心操作实现

1. 节点申请与初始化

申请新节点时,需要分配内存并检查是否成功。对于带头结点的循环链表,新节点初始化时可以先自环,或者在插入时再调整。

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;
}

LTNode* LTInit() {
    // 创建一个哨兵节点,数据可以是任意值,这里用 -1 示意
    LTNode* phead = LTBuyNode(-1);
    return phead;
}

2. 销毁链表

销毁时需要遍历所有有效节点释放内存,最后释放头结点。注意使用二级指针或返回值来更新外部指针,防止野指针。

LTNode* LTDestroy(LTNode* phead) {
    assert(phead != NULL);
    LTNode* pcur = phead->next;
    while (pcur != phead) {
        LTNode* next = pcur->next;
        free(pcur);
        pcur = next;
    }
    free(phead);
    return NULL;
}

3. 插入操作

尾插

在尾部插入时,要注意修改指针的顺序。先连接新节点与旧尾节点,再连接新节点与头结点,最后更新头结点的前驱指针。

void LTPushBack(LTNode* phead, LTDataType x) {
    assert(phead);
    LTNode* newnode = LTBuyNode(x);
    
    // 新节点指向当前尾节点
    newnode->prev = phead->prev;
    // 新节点指向头结点
    newnode->next = phead;
    // 当前尾节点指向新节点
    phead->prev->next = newnode;
    // 头结点前驱指向新节点
    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;
}
指定位置后插入

在 pos 节点之后插入,只需将 pos 的后继节点作为桥梁即可。

void LTInsert(LTNode* pos, LTDataType x) {
    assert(pos);
    LTNode* newnode = LTBuyNode(x);
    
    newnode->prev = pos;
    newnode->next = pos->next;
    pos->next->prev = newnode;
    pos->next = newnode;
}

4. 删除操作

尾删与头删

删除时需确保链表非空。删除后记得将断开节点的指针置为 NULL,避免悬垂指针。

void LTPopBack(LTNode* phead) {
    assert(!LTEmpty(phead));
    LTNode* del = phead->prev;
    del->prev->next = phead;
    phead->prev = del->prev;
    free(del);
    del = NULL;
}

void LTPopFront(LTNode* phead) {
    assert(!LTEmpty(phead));
    LTNode* del = phead->next;
    del->next->prev = phead;
    phead->next = del->next;
    free(del);
    del = NULL;
}
删除指定节点

直接修改待删节点的前驱和后继的指针指向,跳过该节点。

void LTErase(LTNode* pos) {
    assert(pos);
    pos->prev->next = pos->next;
    pos->next->prev = pos->prev;
    free(pos);
    pos = NULL;
}

5. 辅助功能

判空

由于是循环链表,当 next 指向自己时,说明只有头结点,即为空。

bool LTEmpty(LTNode* phead) {
    assert(phead);
    return phead->next == phead;
}
打印

从头结点的后继开始遍历,直到回到头结点停止。

void LTPrint(LTNode* phead) {
    LTNode* pcur = phead->next;
    while (pcur != phead) {
        printf("%d -> ", pcur->data);
        pcur = pcur->next;
    }
    printf("\n");
}
查找

顺序遍历查找目标值。

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;
}

顺序表与链表对比

特性顺序表链表(双向)
存储空间物理连续物理不一定连续,逻辑连续
随机访问支持 O(1)不支持,需遍历 O(N)
插入/删除可能需搬移元素 O(N)仅需修改指针,O(1)
空间管理动态扩容,可能有浪费按需申请,无容量限制
适用场景频繁读取,少增删频繁增删,少随机访问

通过上述分析可以看出,双向链表在插入和删除操作上具有显著优势,适合对数据变动频繁的场景;而顺序表则在访问效率上更胜一筹。实际开发中,应根据具体需求选择合适的结构。

目录

  1. 数据结构:双向链表
  2. 概念与结构
  3. 核心概念
  4. 节点定义
  5. 接口设计
  6. 核心操作实现
  7. 1. 节点申请与初始化
  8. 2. 销毁链表
  9. 3. 插入操作
  10. 尾插
  11. 头插
  12. 指定位置后插入
  13. 4. 删除操作
  14. 尾删与头删
  15. 删除指定节点
  16. 5. 辅助功能
  17. 判空
  18. 打印
  19. 查找
  20. 顺序表与链表对比

更多推荐文章

查看全部
  • IntelliJ IDEA 配置 Google Java Format 插件指南
  • 程序员转型大模型:2025 年 AI 职业发展路径分析
  • 互联网产品经理转型人工智能产品经理实战攻略
  • FPGA 时钟约束完全攻略:create_clock 与 create_generated_clock 实战
  • ControlNet-sd21 配置指南与核心参数解析
  • 新版 VS Code 禁用 Ctrl+I 快捷键调用 Copilot AI 功能
  • Python 设计模式实战:单例与工厂模式详解
  • C++ 进阶核心知识点整理
  • 基于 ToClaw 构建 AI 自动化助手:重复任务托管与远程协作实战
  • Qwen2.5-7B-Instruct 大模型 vLLM 推理加速与前端调用实践
  • Python 核心技术点汇总:装饰器、拷贝与数据结构
  • 人工智能大模型项目实战:从需求到落地的全流程指南
  • 双指针算法:从暴力遍历到线性高效重构数组操作
  • AI 工具泛滥时代,为何核心能力反而贬值?
  • Spring Cloud 优雅实现远程调用 - OpenFeign
  • Docker+K8s 微服务部署与运维实战指南
  • OpenClaw 原理详解:自托管 AI 网关架构深度剖析
  • ChatGLM3-6B 模型架构与微调机制深度解析
  • 动态规划:买卖股票的最佳时机 I、II、III 详解
  • 基于 CLIProxyAPI 与 New API 构建统一 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