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

数据结构:双向链表详解与实现

综述由AI生成双向链表是线性表的一种链式存储结构,每个节点包含数据域及前后两个指针域。详细讲解了带头结点的双向循环链表的设计思路,涵盖初始化、销毁、头尾插删、查找及插入删除等核心操作。重点分析了哨兵位的作用及指针修改顺序,并通过对比顺序表阐述了链表在空间灵活性与操作效率上的特点,适合需要深入理解底层数据结构实现的开发者参考。

栈溢出发布于 2026/3/28更新于 2026/6/519 浏览
数据结构:双向链表详解与实现

数据结构:双向链表

链表的分类

常见的链表分类主要包括单向、双向以及循环结构。理解这些分类有助于根据实际场景选择合适的存储方式。

链表分类示意图

循环链表示意图

概念与结构

概念

双向链表通常采用带头结点的设计,这里的'头结点'实际上是一个哨兵位。它不存储有效数据,仅作为链表的固定入口,简化了边界条件的处理(如头插、头删时无需特殊判断空指针)。

注意:哨兵位结点不存储任何有效元素,只是站在这里'放哨的'。

结构定义

在 C 语言中,我们需要定义节点结构和链表操作接口。

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

typedef int LTDataType;

typedef struct ListNode {
    LTDataType data;
    struct ListNode* next; // 指向下一个节点的指针
    struct ListNode* prev; // 指向前一个节点的指针
} LTNode;

为了保持接口的统一性和易用性,我们约定所有增删改查操作均基于带头结点的循环双向链表进行。

// 初始化
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 位置节点

核心实现细节

申请新节点

分配内存时务必检查是否成功,并初始化指针指向自身以形成闭环。

LTNode* LTBuyNode(LTDataType x) {
    LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
    if (newnode == NULL) {
        perror("malloc failed!");
        exit(1);
    }
    newnode->data = x;
    newnode->next = newnode->prev = newnode; // 初始化为自环
    return newnode;
}

初始化与销毁

初始化直接返回哨兵节点,销毁时记得释放哨兵本身并将头指针置空。

LTNode* LTInit() {
    LTNode* phead = LTBuyNode(-1); // -1 仅为哨兵占位符
    return phead;
}

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

插入操作

插入是双向链表最容易出错的地方,指针修改顺序至关重要。如果先改了后继,再改前驱,可能会丢失原节点地址导致断链或内存泄漏。

尾插逻辑:

  1. 新节点的前驱指向当前尾节点(哨兵的前驱)。
  2. 新节点的后继指向哨兵。
  3. 原尾节点的后继指向新节点。
  4. 哨兵的前驱指向新节点。
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;
}

头插逻辑: 同理,注意不要覆盖 phead->next 的地址。

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

删除操作

删除时同样要注意顺序,先断开连接,再释放内存。

尾删: 直接操作哨兵的前驱节点即可。

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

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

辅助功能

判空只需判断头节点的后继是否指向自己。打印则遍历一圈即可。

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. 申请新节点
  8. 初始化与销毁
  9. 插入操作
  10. 删除操作
  11. 辅助功能
  12. 顺序表与链表分析
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Docker 部署 OpenClaw 常见问题排查与自定义模型配置
  • C++11 核心特性详解:列表初始化、右值引用与移动语义
  • 基于 Docker 部署 AI 量化分析平台及波浪理论实战
  • 高校电动车租赁系统设计与实现:SpringBoot+Vue+MySQL
  • Spring Web MVC 入门:从概念到实践
  • Kokoro-TTS跨平台C++移植实战:从Windows到嵌入式终端
  • 集群中继无人机应急通信双层多目标协同优化部署
  • C++ 设计模式详解:创建、结构及行为型核心实现
  • Ubuntu 下 llama.cpp 编译与性能调优实战
  • LeetCode 替换所有问号与提莫攻击解题思路
  • 算法实战:模幂、构造、背包、贪心及堆维护六题精析
  • 企业级图像AIGC技术:Seedream 4.0 模型能力与应用场景
  • AIGC 延迟优化实战:C++ 零拷贝与异步调度方案
  • Python 临床知识问答与检索系统架构设计与实现
  • GPT、LLaMA 与 MOE:自回归模型与混合专家架构演进
  • YOLOv8 车牌定位模型训练与 OpenCV C++ 部署完整指南
  • GESP 2023 年 12 月 C++ 二级认证试题解析(选择题 9-15)
  • C++ STL map 核心解析:从原理到实战应用
  • DeepFace 结合 OpenCV 实现实时情绪分析
  • 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