数据结构之双链表

作者:无忧.芙桃日期:2026/7/16

本篇目标:

1. 学会关于双链表的相关操作

2. 了解链表和顺序表的区别和优点

一、双链表接口实现

1. 双向链表的结构

• 单链表的结点中保存了指向后继结点的地址,所以单链表中找当前结点的后继结点很容易,但要获取当前结点的前驱结点就很麻烦,就只能从头开始往后遍历获取,时间复杂度为 O(n);所以单链表中只有当前结点的指针 pos 时(没有头指针),想要在 pos 之前插入结点和删除 pos 位置结点都是无法实现的。

• 双向链表相比单链表最大的特征是每个结点中多了一个前驱指针,一些场景需要获取当前结点前驱结点的场景中就需要用双链表实现,如:倒着遍历、删除当前结点、在当前结点前插入结点等。

• 双向链表的一些不足是找尾结点依旧不是很方便,另外呢,头尾插入删除考虑的边界依旧比较多。后面的带头双向循环链表可以很好地解决这个问题。

代码块:

1typedef int DCLDataType;
2
3// 双向循环链表结点
4typedef struct DCListNode
5{
6    DCLDataType data;           // 存储数据元素的值
7    struct DCListNode* prev;    // 存放前驱结点的指针
8    struct DCListNode* next;    // 存放后继结点的指针
9} DCListNode;

如图:

• 实践中链表最常见的操作并非在第 i 个位序插入删除数据,而是在头尾插入删除数据,前面的单链表头插头删效率高,可以做到时间复杂度 O(1),但是尾插尾删,需要找到尾结点,时间复杂度为 O(N),也就是说单链表结构适合头插头删,这个在后面单链表作为复杂数据结构的子结构中可以看到主要就使用它的头插头删。

• 双向链表头插头删效率高,确定某个结点位置以后插入删除效率也很高,均可以做到时间复杂度 O(1),但是同样尾插尾删,需要增加一个尾指针,相对麻烦;所以这里我们引用一个新的结构,循环链表可以解决这里的问题。

如图:

• 下图是单向循环链表和双向循环链表,单向循环链表就是让尾结点的 next 指向头结点;双向循环链表就是尾结点的 next 指向头结点,同时头结点的 prev 指向尾结点。

• 实践中双向循环链表非常实用,C++ 标准库(STL)中 list 就是使用的这个结构实现,因为它可以通过头结点的 prev 指针找到尾结点轻松实现尾插尾删。也就是说这个结构头尾插入删除效率都是 O(1),确定某个结点位置以后的插入删除也是。下面我们会重点实现这个结构。

2. 接口函数定义

1#pragma once
2
3#include <stdio.h>
4#include <stdlib.h>
5#include <assert.h>
6
7typedef int DCLDataType;
8
9// 双向循环链表结点
10typedef struct DCListNode
11{
12    DCLDataType data;           // 存储数据元素的值
13    struct DCListNode* prev;    // 存放前驱结点的指针
14    struct DCListNode* next;    // 存放后继结点的指针
15} DCListNode;
16
17// 链表初始化
18DCListNode* DCListInit();
19
20// 销毁链表
21void DCListDestroy(DCListNode* L);
22
23// 获取链表中位序为 i 的结点
24DCListNode* DCListGetElem(DCListNode* L, int i);
25
26//  pos 位置后插入值为 x 的结点
27void DCListInsert(DCListNode* pos, DCLDataType x);
28
29// 删除 pos 位置的结点
30void DCListDelete(DCListNode* pos);
31
32// 头插
33void DCListPushFront(DCListNode* L, DCLDataType x);
34
35// 尾插
36void DCListPushBack(DCListNode* L, DCLDataType x);
37
38// 头删
39void DCListPopFront(DCListNode* L);
40
41// 尾删
42void DCListPopBack(DCListNode* L);
43
44// 打印链表中的元素
45void DCListPrint(DCListNode* L);

我们实现的是带头双向链表

2.1. 初始化

初始化时,我们需要先创建一个头节点,并且前后指针要指向自己。

1// 创建一个结点
2DCListNode* BuyDCListNode(DCLDataType x)
3{
4    DCListNode* newNode = (DCListNode*)malloc(sizeof(DCListNode));
5    if (newNode == NULL)
6    {
7        perror("malloc failed");
8        return NULL;
9    }
10    newNode->data = x;
11    newNode->prev = NULL;
12    newNode->next = NULL;
13    return newNode;
14}
15
16// 链表初始化
17DCListNode* DCListInit()
18{
19    // 创建哨兵头结点
20    DCListNode* head = BuyDCListNode(-1);
21    if (head == NULL)
22    {
23        return NULL;
24    }
25    // 空链表中,头结点的前驱和后继都指向自己
26    head->next = head;
27    head->prev = head;
28    return head;
29}

2.2. 打印链表

打印双链表其实与单链表的过程差不多。

1// 打印链表中的元素
2void DCListPrint(DCListNode* L)
3{
4    assert(L != NULL);
5
6    DCListNode* cur = L->next;
7
8    printf("头结点<->");
9
10    while (cur != L)
11    {
12        printf("%d<->", cur->data);
13        cur = cur->next;
14    }
15
16    printf("头结点\n");
17}

流程图:

2.3. 销毁链表

销毁双链表其实与单链表的过程差不多,但是要记得结尾删除头节点

1// 销毁链表
2void DCListDestroy(DCListNode* L)
3{
4    assert(L != NULL);
5
6    DCListNode* cur = L->next;
7
8    // 依次释放所有数据结点
9    while (cur != L)
10    {
11        DCListNode* next = cur->next;
12
13        free(cur);
14        cur = next;
15    }
16
17    // 最后释放哨兵头结点
18    free(L);
19}

2.4. 查找节点

查找节点时,我们需要返回这个节点,这是为了方便后面增删操作。

1// 获取链表中位序为 i 的结点
2// i  0 开始,i = 0 表示第一个数据结点
3DCListNode* DCListGetElem(DCListNode* L, int i)
4{
5    assert(L != NULL);
6    assert(i >= 0);
7
8    DCListNode* cur = L->next;
9    int j = 0;
10
11    while (cur != L && j < i)
12    {
13        cur = cur->next;
14        j++;
15    }
16
17    // 如果 cur == L,说明 i 超出了链表范围
18    assert(cur != L);
19
20    return cur;
21}

流程图:

2.5. 插入

• 在 pos 结点之后插入一个新结点 newNode,这里要改四个指针的链接关系,要注意的是一定不能先动 pos->next = newNode,否则就会找不到 pos 的后继结点。建议把 pos->next = newNode; 放到最后就不会出乱子。

• pos 可以指向任意结点(包括头结点),不需要考虑 pos 前一个或者后一个为空的情况。如果是双向链表(非循环),要注意的是pos 为尾结点时,需要考虑 pos->next 为空的情况,并且 pos 不能为头结点。

如图

1//  pos 结点后插入值为 x 的结点
2void DCListInsert(DCListNode* pos, DCLDataType x)
3{
4    assert(pos != NULL);
5
6    DCListNode* newNode = BuyDCListNode(x);
7    assert(newNode != NULL);
8
9    DCListNode* next = pos->next;
10
11    // 将新结点连接到 pos  next 之间
12    pos->next = newNode;
13    newNode->prev = pos;
14
15    newNode->next = next;
16    next->prev = newNode;
17}

有了这个插入,那么头插和尾插就变得比较简单了,如代码:

1// 头插
2void DCListPushFront(DCListNode* L, DCLDataType x)
3{
4    assert(L != NULL);
5
6    DCListInsert(L, x);
7}
8
9// 尾插
10void DCListPushBack(DCListNode* L, DCLDataType x)
11{
12    assert(L != NULL);
13
14    DCListInsert(L->prev, x);
15}

2.6. 删除

• 这里改动两个指针链接关系即可,注意一定要画图捋清楚指针间的关系,否则很容易乱,这两个链接指针关系没有前后顺序。

• pos 可以指向除了头结点以外的任意结点,不需要考虑 pos 前一个或者后一个为空的情况。如果是双向链表(非循环),要注意的是 pos 为尾结点时,需要考虑 pos->next 为空的情况。

1// 删除 pos 结点
2// 注意:pos 必须是有效数据结点,不能是哨兵头结点
3void DCListDelete(DCListNode* pos)
4{
5    assert(pos != NULL);
6    assert(pos->prev != NULL);
7    assert(pos->next != NULL);
8
9    DCListNode* prev = pos->prev;
10    DCListNode* next = pos->next;
11
12    //  pos 的前驱和后继直接连接
13    prev->next = next;
14    next->prev = prev;
15
16    // 避免留下无效连接
17    pos->prev = NULL;
18    pos->next = NULL;
19
20    free(pos);
21}
22

有了这个删除,那么头删和尾删就变得比较简单了,如代码:

1// 头删
2void DCListPopFront(DCListNode* L)
3{
4    assert(L != NULL);
5
6    // 空链表不执行删除
7    if (L->next == L)
8    {
9        return;
10    }
11
12    DCListDelete(L->next);
13}
14
15// 尾删
16void DCListPopBack(DCListNode* L)
17{
18    assert(L != NULL);
19
20    // 空链表不执行删除
21    if (L->prev == L)
22    {
23        return;
24    }
25
26    DCListDelete(L->prev);
27}

2.7. 测试代码

1#include "DCList.h"
2
3int main()
4{
5    // 初始化链表
6    DCListNode* L = DCListInit();
7
8    // 头插
9    DCListInsert(L, 1);
10    DCListInsert(L, 2);
11    DCListInsert(L, 3);
12    DCListInsert(L, 4);
13    DCListPrint(L);
14
15    // 尾插
16    DCListInsert(L->prev, 5);
17    DCListInsert(L->prev, 6);
18    DCListInsert(L->prev, 7);
19    DCListInsert(L->prev, 8);
20    DCListPrint(L);
21
22    // 头删
23    DCListDelete(L->next);
24    DCListDelete(L->next);
25    DCListPrint(L);
26
27    // 尾删
28    DCListDelete(L->prev);
29    DCListDelete(L->prev);
30    DCListPrint(L);
31
32    // 中间插入
33    DCListInsert(DCListGetElem(L, 2), 100);
34    DCListPrint(L);
35
36    // 中间删除
37    DCListDelete(DCListGetElem(L, 2));
38    DCListPrint(L);
39
40    // 销毁链表
41    DCListDestroy(L);
42
43    return 0;
44}

二、顺序表和链表的比较

1. 对比

顺序表和链表都属于线性表,它们的逻辑结构都是线性结构。二者最大的区别在于存储结构,也就是物理结构不同。

顺序表使用一段连续的物理空间存储数据;链表中的结点可以分散存储在任意位置,各个结点通过指针连接起来。

<1>.顺序表

优点:

  1. 支持下标随机访问,访问任意位置元素的时间复杂度为 O(1)
  2. 适合排序、二分查找、堆和优先级队列等操作。
  3. 数据连续存储,CPU 缓存命中率较高。
  4. 通常不会产生大量零散的内存碎片。

缺点:

  1. 在中间或头部插入、删除数据时,需要移动后面的元素,时间复杂度通常为 O(N)
  2. 只有尾插和尾删在不扩容等理想情况下,时间复杂度可以达到 O(1)
  3. 空间不足时需要扩容,扩容时通常需要申请新空间并复制原有数据。
  4. 扩容后可能会存在一定的空间浪费

<2>.链表

优点:

  1. 按需申请和释放结点空间,不需要像顺序表一样整体扩容。
  2. 在已经确定插入或删除位置的情况下,双向链表可以O(1) 时间内完成插入和删除
  3. 插入和删除时不需要整体移动其他元素。

缺点:

  1. 不支持下标随机访问,访问第 i 个结点通常需要从头遍历,时间复杂度为 O(N)
  2. 每个结点需要额外保存指针,会增加空间开销
  3. 结点在内存中不连续,CPU 缓存命中率通常低于顺序表。
  4. 频繁申请和释放小块内存,可能产生内存碎片

需要特别注意一句:链表的插入和删除是 O(1),前提是已经找到了对应结点。

对比项顺序表链表
物理存储连续不连续
随机访问O(1)O(N)
中间插入删除O(N)已知位置时 O(1)
扩容需要不需要整体扩容
额外空间较少需要保存指针
缓存命中率较高较低

总结:大量随机访问、排序和尾插时,更适合顺序表;频繁在已知位置插入和删除时,更适合链表。


数据结构之双链表》 是转载文章,点击查看原文


相关推荐


Android 面试系列:Kotlin 协程的 delay 到底发生在哪个线程?
潜龙勿用之化骨龙2026/7/8

在开发中,我们经常写出这样的代码: mainScope.launch { log("start") delay(1000) log("end") } 这段代码看起来非常简单,但它隐藏了一个非常经典的问题: delay 这 1 秒到底发生在哪个线程? 主线程前后都在执行,那中间谁在“等时间”? 结论 delay 从来不占用线程等待,它是一次“挂起 + 时间注册 + 调度恢复 + 状态机推进”的过程。 中间没有任何业务线程在 sleep,也没有线程在阻塞计时。


别再只会用 cron:Linux systemd Timer 定时任务实战详解
唐青枫2026/6/30

简介 Linux 上提到定时任务,最先想到的通常是 cron。 cron 足够简单,也足够稳定,但任务一旦涉及日志、启动依赖、超时控制、错过后补跑、运行用户和资源限制,单独一行 crontab 很快就会变得难以维护。 systemd Timer 提供了另一套方案: .timer 负责决定什么时候执行 .service 负责决定执行什么、以什么方式执行 例如,每天凌晨备份一次应用数据,可以拆成两个单元: myapp-backup.timer | | 到达触发时间


火山 DTS 正式支持 MySQL 同步到 Milvus , 解决业务库到向量库最后一公里
火山引擎Agent社区2026/6/21

这两年,大模型、智能问答越来越多地落到实际业务里。很多企业在推进过程中慢慢发现,影响 AI 应用落地效率的,除了模型本身能力之外,数据链路是否能顺畅跑通,也同样非常关键。 目前,企业大部分的业务数据库依然在关系型数据库中,而AI应用对支撑语义检索、相似召回的向量数据库有着更强的依赖。怎么把结构化业务数据稳定、持续地同步到向量数据库,正在成为不少企业建设 AI 数据底座时绕不开的问题。 现在,火山引擎 DTS 正式支持 MySQL 同步到 Milvus,帮助企业快速打通从业务数据库到向量数据库的数


计算机网络基础:在 P2P 对等方中搜索对象
梁辰兴2026/6/13

📌目录 ⚖️ 在P2P对等方中搜索对象:去中心化网络的信息发现机制🎯 一、P2P搜索问题概述:去中心化带来的挑战(一)搜索问题的本质(二)搜索算法设计目标(三)搜索算法的分类体系 📦 二、无结构P2P网络中的搜索机制(一)泛洪查询机制(二)随机漫步搜索(三)迭代加深搜索(四)Gossip协议搜索(五)向量时钟与语义搜索 🌐 三、分布式哈希表:结构化搜索的突破(一)DHT的基本原理(二)Chord算法详解(三)CAN算法详解(四)Kademlia算法详解(五)Pastry与T


真正值钱的 AI 小工具,可能只是帮人少打一遍字
深海恶霸Grace2026/6/6

我有个朋友是做财务兼采购的。 他最近有个特别烦的工作: 整理各种报价单信息。 有时候是供应商发来的截图。 有时候是一张图片。 有时候干脆就是一段文字描述。 最后这些东西都要被他重新整理进 Excel。 项目名称、规格、数量、单位、单价、总价。 听起来不难。 但真正做起来,很折磨。 因为这不是一道复杂题。 这是重复劳动。 你得盯着图片看一眼,再切到 Excel 里打一条。 再回来看一眼,再打一条。 遇到数字多一点、截图糊一点、格式乱一点的时候,眼睛真的会看花。 最烦的是,录完之后还不能放心。 因为


Sqoop 安装完整教程(基于 WSL2 + Ubuntu 24.04)
穆金秋2026/5/30

本教程详细介绍了在WSL2+Ubuntu24.04环境下安装配置Sqoop1.4.7的完整流程: 环境准备 Java8+、Hadoop3.3.6、MySQL8.0.45已安装验证命令:java -version/hadoop version/mysql --version 安装步骤 下载Sqoop1.4.7并解压到/usr/local配置环境变量(SQOOP_HOME和PATH)安装MySQL JDBC驱动到Sqoop/lib目录解决依赖问题(commons-lang等jar包)


Gogs: 打造属于你自己的轻量级 Git 服务
修己xj2026/5/8

在软件开发的世界里,Git 已经成为版本控制的事实标准。GitHub、GitLab 等平台提供了强大的托管服务,但有时候,我们需要一个完全属于自己的私有 Git 仓库——可能是为了代码安全,可能是为了定制化需求,可能是为了集成到现有服务中,也可能只是想在自己的服务器上搭建一个个人代码库。开源gitlab有点重,最近我在GitHub上发现了一个轻量级项目Gogs。 什么是 Gogs? Gogs 是一个用 Go 语言编写的自助 Git 托管服务。它的目标是以最简单、最轻松的方式搭建一个简单、稳定且


【系统架构师案例题-知识点】数据库与缓存设计
roman_日积跬步-终至千里2026/4/28

本文聚焦系统架构师案例题中的数据库与缓存设计,重点说明关系型数据库设计、NoSQL 选型、分库分表、读写分离、缓存策略、缓存故障模式以及缓存与数据库一致性问题,并结合电商、支付、内容平台、搜索系统、推荐系统等真实软件行业场景说明这些技术为什么会出现、各自解决什么问题、工程上该如何取舍。 阅读时可以按三个层次把握:先理解数据为什么会成为瓶颈,再理解数据库和缓存分别解决哪一类问题,最后把题干中的业务信号翻译成卷面表达。 一、先建立整体认识 数据库与缓存设计的核心,不是“会不会背名词”,而是看清系统到


Visual Studio 与 Visual Studio Code 区别
日更嵌入式的打工靓仔2026/4/20

特性Visual Studio (VS)Visual Studio Code (VS Code)本质类型集成开发环境 (IDE)轻量级源代码编辑器核心定位大型、复杂的项目开发(Windows、游戏、企业级应用)快速编辑、脚本编写、Web/云开发主要平台Windows、macOS (功能有差异)Windows、macOS、Linux占用空间大 (安装需要几GB到几十GB空间)小 (安装包约100MB以下)性能/速度启动和加载大型项目较慢启动迅速,打开文件极快价格社区版免费;专业版/企业版付费完全免


OpenClaw Windows 安装详细教程
超低空2026/4/11

OpenClaw(前身为 ClawdBot)是一款本地托管的个人 AI 助手系统,可以通过网关控制平面连接到 WhatsApp、Telegram、Discord 等常见通讯软件,并在本地运行各种工作流。 由于 OpenClaw 深度依赖底层系统的进程管理和文件监听,直接在 Windows 原生环境下运行可能会遇到一些限制。因此,官方推荐使用 WSL2(Windows Subsystem for Linux) 或 Docker 来进行安装。以下是详细的安装教程和避坑指南。 安装方式优缺点对比 在

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读