跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
C算法

数据结构:双向循环链表详解

综述由AI生成详细讲解了数据结构中的双向循环链表。内容包括链表分类、双向链表的概念与结构(哨兵位)、基本操作(尾插、头插、尾删、头删、指定位置插入删除、查找、打印)及销毁的实现原理。提供了基于 C 语言的完整代码示例(List.h, List.c, test.c),展示了如何通过一级指针管理带头节点的双向循环链表,确保接口一致性与内存安全。

小熊软糖发布于 2026/3/22更新于 2026/5/413 浏览
数据结构:双向循环链表详解

数据结构:双向循环链表详解

链表分类

链表总共有八种结构,实际中最常用的有这两种结构:单链表和双向带头循环链表(即双链表)。

  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。结构虽然复杂,但是使用代码实现以后会发现结构会带来很多优势。

双向链表

概念

这里的 head 充当什么作用呢? 带头链表里的头结点,实际为'哨兵位',哨兵位结点不存储任何有效元素,只是站在这里'放哨的'。

结构

根据双链表概念,定义结构两个指针分别指向下一个结点和前一个结点的地址,并且每一个结点都能存储数据 data。

申请结点
LTNode* LTBuyNode(LTDataType x) {
    LTNode* node = (LTNode*)malloc(sizeof(LTNode));
    if (node == NULL) {
        perror("malloc fail!");
        exit(1);
    }
    // 申请成功
    node->data = x;
    node->next = node->prev = node;
    return node;
}

创建头节点

根据概念创建一个头结点起到'放哨'的作用,这个结点可随意给值。 为什么用一级指针接收呢?保证哨兵位节点不能被删除(若被删除则不是双链表),节点的地址也不能发生改变,因此传一级最合适。

LTNode* LTInit() {
    // 给双向链表创建一个哨兵位
    LTNode* phead = LTBuyNode(-1);
    return phead;
}

尾插、头插

尾插

首先,通过函数创建一个新节点,并将其数据域设置为输入的值 x,得到新节点的指针 newnode。 设置新结点的指针以插入到尾部。由于这是一个循环链表,尾节点的 next 指向头节点,所以将新节点 newnode 的 prev 指针指向前一个结点。 更新原链表尾节点的指针,使其 next 指针指向新节点 newnode,这样新节点就被正确地链接到链表的末尾。 最后,调整原头节点 phead 的 prev 指针,使其指向新节点 newnode,确保链表的循环特性仍然保持,即尾节点的 next 始终指向头节点。

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

头插

头插与尾插的方法类似。

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

尾删、头删

尾删

  1. 保证双链表有效(即至少有一个哨兵位)。
  2. 定义结点 del 保存尾结点,让尾结点的前一个结点的 next 指向头结点。
  3. 头结点指向 del 的前一个结点。
  4. 释放尾结点 del。
void LTPopBack(LTNode* phead) {
    assert(phead && phead->next != phead);
    LTNode* del = phead->prev;
    del->prev->next = phead;
    phead->prev = del->prev;
    free(del);
    del = NULL;
}

头删

头删与尾删的方法类似。

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

指定位置插入、删除结点

指定位置插入结点

  1. 指向结点是否有效,若无效则断言。
  2. 创建新结点,newnode 的 next 指向未更新链表指定位置的下一个结点;newnode 的 prev 指向指定结点。
  3. 未更新链表指定位置的下一个结点指向 newnode 结点;指定结点指向 newnode 结点。
void LTInsert(LTNode* pos, LTDataType x) {
    assert(pos);
    LTNode* newnode = LTBuyNode(x);
    newnode->next = pos->next;
    newnode->prev = pos;
    pos->next->prev = newnode;
    pos->next = newnode;
}

指定位置删除结点

删除指定结点,想要形参的改变影响到实参,那么传二级指针不是更好吗?事实的确如此,那么我们为什么在这里是传一级指针呢?目的是为了保证接口一致性。可以发现,我们前面函数的实现大部分都是通过传一级指针实现双链表功能,如果这里突然传二级指针会造成疑惑。 需要注意的是:删除指定结点后,由于形参不能改变实参,因此需要我们手动置指定结点为 NULL。

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

查找结点

和单链表一样,我们只需要遍历原链表查找结点,若没找到则返回 NULL。

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

链表打印

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

双链表销毁

  1. 定义 pcur 指向头结点的下一个结点,并遍历双链表。
  2. 每次释放 pcur 结点,并让 pcur 指向下一个结点,直到 pcur 与头结点 phead 相遇为止。

为了保证接口的一致性,这里同样采用传一级指针,需要手动置双链表的头结点为 NULL。

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

完整代码实现

List.h

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int LTDataType;

// 定义双向链表节点的结构
typedef struct ListNode {
    LTDataType data;
    struct ListNode* next;
    struct ListNode* prev;
} LTNode;

// 声明双向链表中提供的方法
LTNode* LTInit();
void LTPrint(LTNode* phead);
void LTPushBack(LTNode* phead, LTDataType x);
void LTPushFront(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);
void LTPopFront(LTNode* phead);
void LTInsert(LTNode* pos, LTDataType x);
void LTErase(LTNode* pos);
LTNode* LTFind(LTNode* phead, LTDataType x);
void LTDestroy(LTNode* phead);

List.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"

// 申请节点
LTNode* LTBuyNode(LTDataType x) {
    LTNode* node = (LTNode*)malloc(sizeof(LTNode));
    if (node == NULL) {
        perror("malloc fail!");
        exit(1);
    }
    node->data = x;
    node->next = node->prev = node;
    return node;
}

// 初始化
LTNode* LTInit() {
    LTNode* phead = LTBuyNode(-1);
    return phead;
}

// 打印双链表
void LTPrint(LTNode* phead) {
    assert(phead);
    LTNode* pcur = phead->next;
    while (pcur != phead) {
        printf("%d->", pcur->data);
        pcur = pcur->next;
    }
    printf("\n");
}

// 尾插
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->prev = phead;
    newnode->next = phead->next;
    phead->next->prev = newnode;
    phead->next = newnode;
}

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

// 头删
void LTPopFront(LTNode* phead) {
    assert(phead && phead->next != phead);
    LTNode* del = phead->next;
    phead->next = del->next;
    del->next->prev = phead;
    free(del);
    del = NULL;
}

// 在 pos 位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x) {
    assert(pos);
    LTNode* newnode = LTBuyNode(x);
    newnode->next = pos->next;
    newnode->prev = pos;
    pos->next->prev = newnode;
    pos->next = newnode;
}

// 删除 pos 节点
void LTErase(LTNode* pos) {
    assert(pos);
    pos->next->prev = pos->prev;
    pos->prev->next = pos->next;
    free(pos);
    pos = NULL;
}

// 查找节点
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;
}

// 销毁节点
void LTDestroy(LTNode* phead) {
    assert(phead);
    LTNode* pcur = phead->next;
    while (pcur != phead) {
        LTNode* next = pcur->next;
        free(pcur);
        pcur = next;
    }
    free(phead);
    phead = NULL;
}

test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "List.h"

void ListTest() {
    LTNode* plist = LTInit();
    LTPushBack(plist, 1);
    LTPushBack(plist, 2);
    LTPushBack(plist, 3);
    LTPushFront(plist, 4);
    LTPrint(plist);
    LTPopBack(plist);
    LTPrint(plist);
    LTNode* find = LTFind(plist, 1);
    LTInsert(find, 4);
    LTPrint(plist);
    LTErase(find);
    find = NULL;
    LTPrint(plist);
    LTDestroy(plist);
    plist = NULL;
}

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

目录

  1. 数据结构:双向循环链表详解
  2. 链表分类
  3. 双向链表
  4. 概念
  5. 结构
  6. 申请结点
  7. 创建头节点
  8. 尾插、头插
  9. 尾插
  10. 头插
  11. 尾删、头删
  12. 尾删
  13. 头删
  14. 指定位置插入、删除结点
  15. 指定位置插入结点
  16. 指定位置删除结点
  17. 查找结点
  18. 链表打印
  19. 双链表销毁
  20. 完整代码实现
  21. List.h
  22. List.c
  23. test.c
  • 💰 8折买阿里云服务器限时8折了解详情
  • GPT-5.5 超高智商模型1元抵1刀ChatGPT中转购买
  • 代充Chatgpt Plus/pro 帐号了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • NewStarCTF2025-Week2-Web
  • DeepSeek 深度使用指南:提示词技巧与本地知识库搭建
  • 生成式 AI 生态系统实践与案例深度分析
  • Gemini、ChatGPT、Qwen、豆包、Claude五大主流AI模型深度对比
  • LeetCode 热题 100 快速通关指南与核心算法模板
  • SpringBoot 多级缓存实战:Redis 与 Caffeine 结合优化 API 性能
  • 开源智能排产系统 JVS-APS:算法驱动与低代码融合
  • 基于 Java 的大数据实时流处理在工业物联网设备状态监测中的应用与挑战
  • API 基本概念、功能分类、认证方式、使用方法和开发流程
  • XGBoost + SHAP 回归预测与可解释性分析实战
  • Spring Boot 数据导入导出与报表生成实战
  • PDF 压缩工具:纯前端开源本地压缩方案及实现思路
  • 利用 Web Unlocker 与 n8n 实现自动化资讯采集推送
  • Python 驱动浏览器自动化:Playwright + AI 最佳实践
  • Java 构建大模型应用实践:基于 OpenNLP 与 ONNX Runtime 的推理方案
  • 基于 Web Unlocker 与 n8n 的自动化资讯采集推送系统
  • 自进化医疗智能体:动态记忆与持续运行的 Python 架构设计
  • Python性能分析:从cProfile到火焰图,精准定位性能瓶颈
  • Trae IDE 搭建 C++ 开发环境及 cppdbg 调试配置指南
  • Live Avatar 部署教程:Gradio Web UI 与 CLI 模式快速上手

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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