链表

https://baike.baidu.com/item/%E9%93%BE%E8%A1%A8/9794473




一、链表的主要用途

        链表的主要用途是支持高效插入与删除、动态内存分配,适用于数据规模不确定或频繁修改的场景。‌‌

‌1、动态大小管理‌:

        无需预先分配固定内存,可按需申请/释放节点,适合数据量变化大的应用(如实现动态数组、内存池)。

2、高效插入/删除‌:

        在已知位置时,时间复杂度为 ‌O(1)‌(仅改指针),远优于数组的 ‌O(n)‌(需搬移元素)。

3、实现其他数据结构‌:

        底层常用于构建栈、队列、哈希表(解决冲突)、图的邻接表等。

4、避免内存碎片限制‌:

        节点可分散存储,不依赖连续内存,适合嵌入式或内存碎片化环境。

‌5、特定算法与系统应用‌:

        如文件系统目录、文本编辑器光标管理(双向链表)、操作系统任务调度、哈希冲突链式存储等。‌‌缺点是‌不支持随机访问‌(查找需 ‌O(n)‌)且‌额外存储指针开销大‌,故不适用于高频随机读取的场景。







二、链表的核心作用

        链表的核心作用是适配动态数据场景,解决数组插入/删除效率低、需要预分配空间的问题‌。以下是具体作用和典型应用场景:

1、核心作用优势

1)、‌高效插入删除‌:

        插入或删除节点只需修改相邻节点的指针,时间复杂度为‌O(1)‌,远优于数组的O(n)(数组需要移动大量后续元素)。

2)、‌动态内存管理‌:

        无需预先确定数据规模,节点可在运行时动态生成,用多少内存分配多少,不会造成空间浪费。

3)、‌灵活适配不连续内存‌:

        不需要整块连续内存空间,可利用内存中的零散碎片,提升内存利用率。

2、典型应用场景

1)、‌底层数据结构实现‌:

        常用来实现栈、队列、哈希表、图邻接表等基础数据结构,其中哈希表会用链表处理哈希冲突(存储相同哈希值的数据)。

2)、‌操作系统内核‌:

        操作系统的就绪表、等待表、挂起表等任务调度场景,普遍使用双向循环链表管理任务,方便快速增删任务节点。

3)、‌高频增删业务场景‌:

        如LRU缓存淘汰策略、音乐/视频播放列表,需要频繁增减条目,链表能保证操作高效。

4)、‌编程语言内置容器‌:

        Java的LinkedList、C++的std::list等内置容器底层都基于链表实现,提供动态增删能力。

3、局限性

        链表也存在短板:访问指定元素需要从头开始遍历,随机访问的时间复杂度为‌O(n)‌,比数组的O(1)慢;同时每个节点需要额外存储指针,空间开销更大。因此适合‌频繁增删、较少随机访问‌的场景。

三、链表和数组的具体适用场景差异

链表和数组的核心差异可以从5个关键维度对比,结合场景选择如下:

对比维度

数组

链表

‌存储方式‌

 内存连续,无需额外指针存储关联

 内存分散不连续,每个节点需要额外指针存储下一个节点地址

‌访问效率‌

 支持索引随机访问,时间复杂度‌O(1)‌,访问速度快

 只能从头遍历,随机访问时间复杂度‌O(n)‌,访问速度慢

‌增删效率‌

 插入/删除需要移动大量元素,时间复杂度‌O(n)‌,效率低

 仅需修改指针,已知节点位置时增删时间复杂度‌O(1)‌,效率高

‌空间特性‌

 需要提前申请固定大小,提前预留空间易浪费,扩容需要复制整体元素

 动态分配空间,用多少申请多少,无空间浪费,内存利用率高

‌缓 存

友好性‌

 连续内存空间能提升缓存命中率,访问更快

 分散内存导致缓存命中率低,访问开销更大

‌‌‌

场景选择参考

1、‌选数组‌:满足以下任意一种场景优先用数组:

(1)、需要频繁随机访问元素,对访问速度要求高

(2)、元素总量提前确定,不会频繁变化

(3)、对内存空间利用率要求高,不希望有额外指针开销

(4)、典型场景:矩阵运算、排序算法底层、静态数据缓存、批量同类型数据存储

2、‌选链表‌:满足以下任意一种场景优先用链表:

(1)、需要频繁在任意位置插入、删除元素

(2)、元素总量不确定,运行过程中规模变化大

(3)、无法申请到足够大的连续内存空间

(4)、典型场景:任务调度队列、LRU缓存、播放列表、动态栈/队列、哈希冲突处理







四、如何根据场景选择适合的数据结构

        选择适合的数据结构是解决编程问题的核心步骤,主要需要从‌操作频率、数据规模、内存限制、数据关系‌四个维度进行综合权衡。以下是具体的决策逻辑和常见场景映射:

1. 核心决策维度

A、操作类型(高频操作是什么?)

‌(1)、频繁查找/随机访问‌ → 优先选 ‌数组‌ 或 ‌哈希表‌。

‌(2)、频繁插入/删除‌ → 优先选 ‌链表‌。

‌(3)、频繁排序/查找极值‌ → 优先选 ‌堆(Heap)‌ 或 ‌二叉搜索树(BST)‌。‌

(4)、频繁前缀匹配/字符串处理‌ → 优先选 ‌字典树(Trie)。‌‌

B、数据规模与动态性

‌(1)、数据量固定或已知‌ → ‌数组‌(内存连续,效率高)。‌

(2)、数据量未知且剧烈波动‌ → ‌链表‌ 或 ‌动态数组(如 ArrayList)‌。‌

(3)、海量数据(内存放不下)‌ → 考虑 ‌外部排序‌ 或基于磁盘的 ‌B+树‌。

C、空间复杂度(内存是否敏感?)

‌(1)、内存极度受限‌ → ‌数组‌(无额外指针开销)。‌

(2)、内存充足,追求开发效率‌ → ‌哈希表‌ 或 ‌高级容器‌。‌

D、数据间的逻辑关系

‌(1)、线性关系‌ → 数组、链表、栈、队列。

‌(2)、层级/树状关系‌ → 树(二叉树、红黑树、B+树)。

‌(3)、网状/多对多关系‌ → 图(邻接矩阵或邻接表)。

2. 常见场景与数据结构映射表

业务场景/需求特征

推荐数据结构

理由

‌快速查找特定ID/Key‌

‌哈希表 (HashMap)‌

平均时间复杂度 O(1),查找速度最快。

‌需要保持插入顺序 + 快速查找‌

‌LinkedHashMap‌

结合哈希表的查找优势和链表的顺序优势。

‌频繁在头部/中间增删数据‌

‌链表 (LinkedList)‌

避免移动大量元素,O(1) 完成指针修改。

‌实现“最近最少使用”缓存 (LRU)‌

‌哈希表 + 双向链表‌

哈希表负责快速定位,链表负责维护访问顺序。

‌表达式求值 / 括号匹配 / 递归模拟‌

‌栈 (Stack)‌

符合“后进先出” (LIFO) 的逻辑特性。

‌任务调度 / 广度优先搜索 (BFS)‌

‌队列 (Queue)‌

符合“先进先出” (FIFO) 的逻辑特性。

‌获取最大/最小值 / 优先级任务‌

‌堆 (Heap/Priority Queue)‌

能在 O(log n) 时间内获取极值,适合Top K问题。

‌数据库索引 / 文件系统目录‌

‌B+树 / B树‌

减少磁盘I/O次数,适合范围查询和大规模数据存储。

‌自动补全 / 词频统计前缀‌

‌字典树 (Trie)‌

利用字符串公共前缀节省空间,检索效率高于哈希表。

‌社交网络关系 / 地图路径规划‌

‌图 (Graph)‌

能够表达节点之间复杂的多对多连接关系。

3. 实战选择建议(简化版)

1)、‌默认首选数组‌:

        如果不确定选什么,且数据量不大、增删不频繁,直接用数组。它内存紧凑、CPU缓存友好,实际运行速度往往最快。

2)、‌查得多用哈希‌:

        如果核心需求是“通过Key快速找到Value”,毫不犹豫选哈希表。

3)、‌改得多用链表‌:

        如果需要在列表中间频繁插入或删除,且数据量较大,选链表。

4)、‌有序用树/堆‌:

        如果数据需要保持有序,或者需要频繁获取最大/最小值,选择平衡二叉搜索树(如红黑树、TreeMap)或堆。

5)、‌层次用树,网状用图‌:

        根据数据本身的逻辑结构决定,不要强行用线性结构去模拟复杂关系。

4. 避坑指南

1)、‌不要过度优化‌:

        在数据量较小(如小于1000)时,数组和链表的性能差异几乎可以忽略,此时应选择代码更简单、更易维护的结构(通常是数组)。

2)、‌注意语言特性‌:

        Java中的 ArrayList 底层是数组,LinkedList 底层是链表;Python的 list 底层是动态数组。了解语言内置容器的底层实现,能帮你做出更准确的选择。







五、如何权衡数组和链表的优缺点

        权衡数组和链表的优缺点,核心是‌围绕「访问效率」和「增删效率」做 trade-off(取舍)‌,关键看你的场景中「访问」和「增删」哪个更频繁,结合内存、性能要求综合判断,以下是清晰的权衡逻辑:

1. 先抓住核心矛盾:二者优缺点完全互补

优势维度

数组

链表

随机访问效率

✅ ‌O(1),极快‌

❌ ‌O(n),慢‌

任意位置增删效率

❌ ‌O(n),慢(需要移动元素)‌

✅ ‌O(1),快(仅改指针)‌

内存空间利用率

❌ 预分配浪费/需扩容复制,利用率低

✅ 动态分配,无浪费,利用率高

CPU缓存友好性

✅ 连续内存,缓存命中率高,实际访问更快

❌ 分散内存,缓存命中率低,访问开销大

空间额外开销

✅ 无额外指针,仅存数据

❌ 每个节点额外存指针,总空间开销更大

2. 分场景权衡决策逻辑

        可以按以下优先级依次判断,快速做出选择:

第一步:看「什么操作是高频」

        如果‌查找/随机访问是核心需求,增删很少‌:直接选数组。哪怕数据量小,增删慢的影响可以忽略,数组的访问速度优势会带来更大收益。

        例:存储固定的配置列表、需要按下标快速取元素、排序算法的底层存储。

        如果‌频繁插入/删除元素,访问多为顺序遍历‌:优先选链表。增删的效率收益远大于访问慢的损失。

        例:LRU缓存的队列、操作系统任务链表、频繁增删的播放列表。

第二步:看「数据规模是否确定」

        如果‌提前能确定数据总量,不会大幅变化‌:选数组,提前分配好空间,没有动态分配的开销,性能更稳定。

        如果‌数据规模完全不确定,运行时波动极大‌:选链表,不会因为预分配空间不够扩容,也不会浪费预留的内存。

第三步:看「内存约束」

        如果‌内存紧张,对空间开销敏感‌:选数组,没有额外指针的空间浪费,内存利用率更高。

        如果‌内存充足,碎片化比较严重,找不到连续大块内存‌:选链表,利用零散内存就能存储。

3. 容易混淆的误区纠正

‌        不要迷信“链表增删快”:只有找到插入位置后才是O(1)‌

        如果需要先遍历找到插入位置,整体复杂度还是O(n),和数组差别不大。只有头尾增删、或已经定位到节点时,链表才真的快。

‌        现代CPU架构下,数组实际性能往往比链表好‌

        哪怕理论上链表增删是O(1),但因为缓存命中率极低,实际运行速度可能远慢于数组,只有数据量极大时,链表的优势才会体现。

‌        大多数场景不需要硬选:动态数组(如Java ArrayList、Python list)已经兼顾两者优势‌

        动态数组本质是数组,支持自动扩容,大部分常规开发场景直接用它足够,不需要刻意用链表。

一句话总结权衡口诀

‌        查得多、规模定、内存紧,选数组;改得多、规模变、缺连续,选链表。







六、std::vector和std::list哪个更高效

        在绝大多数现代 C++ 应用场景中,‌std::vector 比 std::list 更高效‌。

        虽然从算法复杂度理论上看,std::list 在中间插入/删除是 O(1),而 std::vector 是O(n),但在实际硬件运行环境中,‌内存局部性(Cache Locality)‌ 对性能的影响往往远超算法复杂度。

以下是详细的效率对比与选型指南:

1. 核心结论:为什么 std::vector 通常更快?

A、‌缓存友好(Cache Friendly)‌:

        (1)、std::vector 的元素在内存中‌连续存储‌。CPU 预取器(Prefetcher)可以高效地将后续数据加载到高速缓存(L1/L2 Cache)中。

        (2)、std::list 的节点在堆上‌分散分配‌。遍历链表时,每次访问下一个节点都可能发生 ‌Cache Miss‌(缓存缺失),导致 CPU 等待内存读取,这种延迟远高于简单的内存拷贝。

B、‌内存分配开销少‌:

        (1)、std::vector 一次性分配一大块连续内存,扩容频率低。

        (2)、std::list 每插入一个元素都要单独进行一次内存分配(new/malloc),频繁的小对象分配不仅慢,还容易导致内存碎片。

C、‌指令流水线优化‌:

        (1)、连续内存允许 CPU 更好地进行分支预测和指令并行执行。

        (2)、链表的指针跳转(Pointer Chasing)会导致流水线停顿。

2. 详细性能对比表

操作场景

std::vector

std::list

谁更高效?

‌随机访问‌ (v[i])

 O(1)O(1)O(1),极快

 O(n)O(n)O(n),需遍历

 ‌Vector 完胜‌

‌尾部插入‌ (push_back)

 摊销 O(1)O(1)O(1),极快

 O(1)O(1)O(1),快

 ‌Vector 略快或持平‌

‌头部插入‌ (push_front)

 O(n)O(n)O(n),需移动所有元素

 O(1)O(1)O(1),仅改指针

 ‌List 胜‌

‌中间插入/删除‌

 O(n)O(n)O(n),需移动元素

 O(1)O(1)O(1),仅改指针*

 ‌视情况而定‌

‌遍历/迭代‌

 极快(缓存命中率高)

 慢(缓存命中率低)

 ‌Vector 完胜‌

‌排序‌(std::sort)

 极快

 不支持随机访问,需用 list::sort

 ‌Vector 完胜‌

‌内存占用‌

 低(无额外指针,可能有预留空间)

 高每个节点多2个指针+对齐开销

 ‌Vector 胜‌

注意‌:std::list 的中间插入 O(1) 前提是‌你已经拥有了指向该位置的迭代器‌。如果你需要先查找位置(O(n)),那么总耗时也是 O(n),且由于缓存不友好,实际运行往往比 vector 更慢。

3. 实测数据参考

        根据多项基准测试(如 Bjarne Stroustrup 的测试或 GitHub 上的 benchmark):

1)、遍历整数列表‌:

        std::vector 比 std::list 快 ‌5~10 倍甚至更多‌。

2)、随机插入‌:

        对于小数据类型(如 int),std::vector 往往仍然更快,因为移动几个字节的内存比分配一个新节点并处理指针要快得多。

3)、大数据对象‌:

        如果元素非常大(如包含大量数据的结构体),移动成本高,此时 std::list 的指针交换优势才会显现。

4. 什么时候应该选 std::list?

        尽管 vector 通常更快,但以下场景 std::list 是更好的选择:

1)、迭代器稳定性要求高‌:

        在遍历过程中需要频繁插入/删除元素,且希望其他迭代器不受影响。vector 插入/删除可能导致所有后续迭代器失效,而 list 只有被删除节点的迭代器失效。

2)、超大对象且频繁中间增删‌:

        如果元素对象非常大(拷贝/移动代价极高),且在容器中间频繁增删,list 避免数据移动的优势会超过缓存劣势。

3)、实现特定数据结构‌:

        如实现 LRU 缓存、队列、或者需要 splice(拼接)操作时,list 的节点链接特性非常有用。

5. 最佳实践建议

1)、默认首选 std::vector‌:

        除非你有明确的理由证明 vector 不适用,否则永远优先使用 vector。它是 C++ 中性能最均衡、最通用的容器。

2)、如果需要频繁头尾增删‌:

        考虑 std::deque,它在头尾增删上是 O(1),且支持随机访问,性能介于 vector 和 list 之间。

3)、如果必须用链表‌:

        确保你真正需要双向链表(std::list)。

        如果只需要单向链表,使用 std::forward_list,它更节省内存。

        不要‌为了“理论上的 O(1) 插入”而盲目选择 list,务必通过 Profiling(性能分析工具)验证实际瓶颈。

6、总结:

(1)、追求极致性能、遍历、随机访问、尾部操作‌ → ‌std::vector‌

(2)、追求迭代器稳定、频繁中间增删大对象、特定算法需求‌ → ‌std::list







七、为什么要用双向链表,双向链表有什么用

        双向链表(Doubly Linked List)是在单向链表的基础上,为每个节点增加了一个‌指向前驱节点(Previous Node)‌的指针。

        它的核心作用是‌解决单向链表“只能前进、不能后退”以及“删除节点必须从头遍历找前驱”的效率瓶颈‌。

以下是双向链表的核心用途及为什么要使用它的详细分析:

1. 核心优势:为什么要用双向链表?

维度

单向链表

双向链表

优势解读

‌遍历方向‌

 只能从头到尾(单向)

 ‌支持双向遍历‌(头↔尾)

 可以从任意节点向前或向后查找,灵活性极高。

‌删除节点‌

 需从头遍历找到前驱节点,复杂度 ‌O(n)‌

 已知节点时,直接通过前驱指针修改,复杂度 ‌O(1)‌

 ‌这是最大的性能优势‌,无需额外遍历即可自我删除。

‌插入节点‌

 需找到前驱节点,复杂度 ‌O(n)‌

 已知位置时,直接修改前后指针,复杂度 ‌O(1)‌

 在任意位置插入更高效。

‌空间开销‌

 小(1个指针)

 ‌大‌(2个指针:next + prev)

 用空间换时间,适合对增删效率要求高的场景。

2. 典型应用场景:双向链表有什么用?

A. LRU 缓存淘汰算法(最经典场景)

1)‌需求‌:

        需要快速访问数据(HashMap),同时需要维护数据的“最近使用顺序”,当缓存满时,淘汰最久未使用的数据(通常在链表尾部)。

2)、‌为什么用双向链表‌:

        当某个数据被访问时,需要将其从当前位置‌移除‌并移动到链表头部。

        如果使用单向链表,移除节点需要从头遍历找到前驱节点,效率低。

(3)、使用双向链表,结合 HashMap 存储节点引用,可以直接通过 prev 和 next 指针在 ‌O(1)‌ 时间内完成节点的移除和重插。

        Java中的 LinkedHashMap 底层就是基于双向链表实现的。

B. 浏览器的前进/后退功能

1)、‌需求‌:

        用户浏览网页时,可以点击“后退”回到上一页,也可以点击“前进”回到下一页。

2)、‌为什么用双向链表‌:

  1. 当前页面作为链表中的一个节点。
  2. “后退”操作即访问 prev 节点。
  3. “前进”操作即访问 next 节点。
  4. 双向结构完美契合这种双向导航的需求。

C. 音乐/视频播放列表

1)、‌需求‌:

支持“上一首”、“下一首”切换,支持在当前歌曲后插入新歌曲,支持删除当前歌曲。

2)、‌为什么用双向链表‌:

  1. 双向遍历支持无缝切换上一首/下一首。
  2. 删除当前正在播放的歌曲时,不需要重新遍历列表,直接调整前后节点的指针即可保持播放顺序连贯。

D. 操作系统内核任务调度

1)、‌需求‌:

        操作系统需要管理成千上万个进程/线程,频繁地进行任务的添加、移除、状态切换(如从“运行态”移到“等待态”)。

2)、‌为什么用双向链表‌:

  1. Linux 内核中的进程控制块(PCB)通常通过双向链表连接。
  2. 当一个进程结束或被挂起时,内核需要迅速将其从就绪队列中移除,双向链表的 O(1) 删除特性至关重要。

E. 实现双端队列(Deque)

1)、‌需求‌:

        支持在头部和尾部都进行高效的插入和删除操作。

2)、‌为什么用双向链表‌:

  1. 单向链表只能在头部高效操作,尾部操作需要遍历。
  2. 双向链表配合尾指针,可以在头尾两端都实现 O(1) 的增删,是实现 std::deque 或 Java ArrayDeque(部分实现)的基础结构之一。

3. 总结:何时选择双向链表?

1)、‌选双向链表‌:

        当你需要‌频繁在任意位置插入/删除节点‌,或者需要‌双向遍历‌(如前进/后退、上一首/下一首)时。

2)、‌不选双向链表‌:

        如果内存非常敏感,且只需要单向遍历、极少删除操作,单向链表或数组可能更合适。

双向链表是用‌额外的空‌一句话总结‌:

间(一个指针)‌换取了‌时间效率(O(1) 删除/插入)‌和‌操作灵活性(双向遍历)‌,是处理动态数据序列的高级工具。







链表动画演示 等比数列 linux视频等

https://blog.csdn.net/dllglvzhenfeng/article/details/126691692

NOI入门数据结构:线性表之链表

https://blog.csdn.net/dllglvzhenfeng/article/details/123785589

信息学奥赛 链表专题

https://blog.csdn.net/dllglvzhenfeng/article/details/130900295

3440:【例77.1】模拟链表

https://blog.csdn.net/dllglvzhenfeng/article/details/141893048

分块、块状链表

https://blog.csdn.net/dllglvzhenfeng/article/details/123007049




Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐