链表应用、优点和缺点

2025年3月17日 | 阅读 12 分钟

链表是计算机科学和编程中广泛使用的一种数据结构。与在内存中存储数据的数组不同,链表由包含数据字段和指向其他节点的指针的节点组成。这些节点之间的连接构成了它们被称为链表的原因。

与固定大小的数组和其他数据结构相比,链表具有优势。在不重新排列内存中所有元素的情况下,从链表中插入和删除元素非常高效。此外,链表可以动态调整大小,在任何给定时刻利用内存。

然而,链表也有其缺点。当元素在链表中不连续存储在内存中时,随机访问的效率会降低。访问、搜索和删除链表上的操作可能需要遍历链表。

理解链表的优势和局限性对于确定何时其适应性和灵活性优于数组的速度至关重要。链表在栈、队列或其他需要高效插入/删除操作、可变大小和元素重排的场景中表现出色。在为应用程序选择数据结构时,评估权衡至关重要。

我们将探讨链表相比于数组和其他数据结构所提供的优势。

我们还将讨论在何种情况下,额外的工作和缺乏访问性可以使链表成为一个有利的选择。理解这一概念将有助于程序员明智地选择链表和其他方法。

applications, Advantages and Disadvantages of Linked List

链表

链表以“节点”的线性序列组织数据。每个节点包含两部分:存储的数据和一个指向列表中下一个节点的引用或“指针”。第一个节点称为头节点,而最后一个节点指向 null,标记列表的结束。

单向链表只允许通过“next”引用从头部到尾部的单向遍历。双向链表包含一个指向前一个节点的附加指针,允许双向遍历。

在结构上,链表本质上是递归的——每个节点都引用另一个链表实例(即后面的其余节点)。这种引用链赋予了链表的灵活性。数组元素根据其索引连续存储在内存中,但链表节点可以分散在内存中。

将节点添加到链表中是 O(1) 的,因为新节点可以存储在内存中的任何位置。与向数组添加元素相比,如果底层数组不够长,后者可能需要移动所有后续元素。

同样,删除节点也很简单——只需更新前一个节点的“next”指针以跳过要删除的节点。数组同样需要移动元素来填补空白。

一个主要的缺点是访问特定索引需要从头开始遍历链表,这是一个 O(N) 操作。数组具有 O(1) 的常数时间随机访问。

因此,链表相对于数组具有互补的优缺点。选择正确的结构取决于访问模式是偏向插入/删除还是随机访问。

算法

要实现一个基本的单向链表,我们需要定义一个 Node 类或结构体,其中包含两个字段:

  1. Data - 存储该节点的实际数据值
  2. Next - 存储指向链表中下一个节点的引用或指针。

此外,我们还需要一个 Linked List 类来跟踪列表的头(指向第一个节点的引用)以及可选的列表大小。

实现链表基本操作的关键步骤是:

插入新节点

  1. 创建一个具有所需数据值的新节点实例
  2. 将新节点的“Next”引用设置为指向列表的当前头节点
  3. 更新 Linked List 实例中的头引用以指向新节点
  4. 增加链表的大小

删除节点

  1. 遍历到要删除节点之前的一个节点
  2. 更新其“Next”引用以跳过要删除的节点,并直接指向下一个节点
  3. 减少链表的大小

搜索节点

  1. 从头节点开始遍历列表
  2. 对于每个节点,将其数据值与目标搜索值进行比较
  3. 如果找到匹配项,则返回该节点引用
  4. 如果到达列表末尾(next == null),则返回 null

链表的主要优点是高效的插入和删除,这得益于节点的动态分配和用于连接它们的指针。权衡是缺乏随机访问,并且需要顺序遍历节点来查找、访问或删除元素。

输出

applications, Advantages and Disadvantages of Linked List

说明

  1. 定义一个 Node 类来表示链表中的每个节点。它包含数据和下一个引用。
  2. 定义一个 LinkedList 类来表示整个列表。它包含指向第一个节点的 head 引用和一个 size 变量。
  3. insertStart() 方法处理在列表开头插入
    • 创建一个带有给定数据的新节点
    • 将其 next 设置为指向当前头节点
    • 将列表的 head 设置为新节点
    • 增加大小
  4. delete() 方法处理删除特定节点
    • 遍历列表,跟踪当前节点和前一个节点
    • 找到目标数据时,调整前一个节点的 next 指针以跳过要删除的节点
    • 减少大小
  5. search() 方法允许查找特定节点
    • 从 head 开始遍历列表,检查每个节点的“data”
    • 如果找到,则返回 Node。否则,如果到达末尾,则返回 None。
  6. Node 和 LinkedList 类封装了链表所需的关键行为——节点之间的引用以及插入、删除和搜索等基本操作。
  7. 通过利用这些类,我们可以在应用程序中创建和操作链表数据结构。

此实现的优点是通过操作节点引用而不是移动数组元素来实现高效的插入和删除。缺点是没有随机访问。

链表应用

实现堆栈和队列

链表为实现堆栈和队列提供了理想的底层数据结构。对于堆栈,我们只需从列表的头部插入和删除。这提供了 O(1) 的 push 和 pop 操作。对于队列,我们在尾部插入并在头部删除,从而利用了高效的插入/删除。动态大小和缺乏随机访问对这些用例来说并不是缺点。

示例用法:链表常用于构建依赖于队列的调用堆栈实现和广度优先搜索树遍历算法。高效的插入/删除和固定大小的缺失使链表成为自然的选择。

动态内存分配

程序使用的堆内存通常通过一种称为空闲链表的数据结构来管理。它跟踪可用于分配的可用内存块。链表允许在空闲链表中高效地插入和删除不同大小的块。当一个块被释放时,它可以很容易地插回空闲链表。

示例用法:GNU C 库使用隐式链表系统来高效管理进程堆并为程序分配动态内存。

文件系统目录遍历

文件系统将目录内容(如文件和文件夹)存储为链表或树。这允许通过操作指针而不是移动数组内容来高效地添加、删除和列出文件。

示例用法:Linux ext2/ext3 文件系统使用双向链表来表示每个目录的内容。新条目可以轻松添加/删除。

音频播放列表管理

音频曲目播放列表可以自然地实现为链表。通过更改节点之间的指向而不是移动数组元素,可以即时重新排序曲目。插入和删除曲目也很高效。

示例用法:Spotify 等应用程序利用链表的优势来实现流畅的拖放式曲目重新排序和高效的播放列表修改。

哈希表链式处理

哈希到同一哈希值的哈希表桶可以将这些条目存储为链表。这提供了比必须调整大小的数组更高效的链式处理。通过插入/删除节点,可以轻松地添加/删除具有相同哈希的条目。

示例用法:Python 的哈希表实现使用链表来链接冲突的条目,从而无需调整数组大小。

图中的邻接列表

图节点可以各自维护其相邻节点的链表。这为稀疏图提供了最佳存储。通过为给定节点的链表添加/删除指针,可以高效地插入/删除边。

示例用法:社交网络好友连接可以按用户存储为链接的好友列表。可以轻松高效地添加新友谊。

因此,链表在许多关键数据结构和依赖于高效插入、删除和重排的应用中发挥其核心优势。链表的通用性和简单性使其成为许多领域的基石。

链表的优点

动态大小

链表可以根据需要扩展和收缩。这在数据量不可预测或可能频繁更改的情况下提供了灵活性。当需要新节点时,内存是动态分配的。

示例用法:使用链表实现的堆栈和队列可以根据应用程序需求进行增长和收缩,而不是预先分配固定容量的数组。

高效插入和删除

向链表添加和删除节点非常高效,因为它只涉及修改几个指针。与需要移动所有元素的数组插入或删除相比,这是一个优势。

示例用法:实现内存分配的空闲存储允许根据需要高效地拆分和合并块。链表在这里优于数组。

内存效率

链表节点只消耗其包含的数据和指针的内存。数组即使在元素未使用的情况下也需要分配连续的内存块。这使得链表在某些用例中内存效率更高。

示例用法:图的邻接列表只为实际边分配内存,而不是分配带有空槽的完整数组。适用于稀疏图。

元素重排

通过修改“next”/“previous”指针,可以轻松地重新排序链表中的元素。重新排序数组需要移动元素,这更复杂。

示例用法:作为链表实现的播放列表允许简单的拖放式曲目重新排序。

灵活性

链表允许从任何位置高效地插入和删除,而不仅仅是两端。数组仅限于从末尾推送/弹出或在中间插入/删除。

示例用法:文件系统可以高效地在目录中插入、删除或查找文件,无论其位置如何。

迭代支持

链表允许通过跟随“next”和“previous”引用进行向前和向后遍历。标准迭代器可以利用节点之间的指针。

示例用法:在实现需要顺序访问的堆栈、队列和其他结构时,可以使用链表迭代器。

并发友好性

多个线程共享访问时,可以安全地添加和删除链表节点。数组在调整大小时需要复杂的同步。

示例用法:共享作业队列可以使用无锁链表操作,而不是锁定整个数组。

缓存友好性

由于数组中的每个元素都引用随机内存地址,因此数组的缓存局部性会随着大小的增大而变差。链表由于线性节点分配而具有更好的局部性。

示例用法:长链表通过将数据保留在 CPU 缓存中而不是主内存中,性能优于巨大的数组。

代码更简单

链表插入和删除只需要更改几个指针。等效的数组操作更长、更复杂。

示例用法:链表实现的堆栈、队列、图等比基于数组的版本更简单、更短。

非连续内存分配

与数组不同,链表节点可以存储在内存中任何可用的地方。与需要足够大的单个连续内存块来容纳所有元素的数组相比,这提供了更大的分配灵活性。链表可以动态地增长和收缩,通过自由内存分配分散的节点。这使得分配失败的可能性更小。

无浪费空间

数组可能预先分配了比所需容量更多的空间以允许增长。这会导致保留的内存未被使用,无法用于其他目的。链表只在任何给定时间消耗其现有节点所需的内存。没有空间浪费,从而更有效地利用可用内存。

无限制大小

数组的大小在创建时是固定的。相比之下,链表没有理论大小限制——只要有可用内存,它们就可以无限增长以容纳新节点。这使得链表在大小无界、不可预测增长的用例中具有优势。唯一的限制是系统可用的总内存。

数据无关

数组要求每个元素都必须是相同的数据类型,而链表节点可以包含任何类型的数据。这提供了更大的灵活性。通过让节点存储 void* 数据指针,同一个链表可以包含不同的数据类型。这种适应性可用于创建异构集合。

递归数据结构

链表节点包含一个指向另一个链表作为下一个元素的指针。这种递归定义使得使用链表连接节点可以优雅而高效地实现复杂的递归数据结构,如树和图。数组无法自然地捕获这些递归关系。

因此,我们可以看到,链表除了在插入和删除方面的效率外,还提供了许多软件工程方面的优势。它们的灵活性、内存使用、并发支持和简单性使其成为许多数据结构和用例的通用工具。

链表的缺点

内存开销

链表节点需要额外的内存来存储指向下一个和上一个节点的指针引用。这种开销意味着链表比存储相同数据的数组占用更多内存。

示例:int 数组只需要连续的空间来存储 int。int 链表需要为每个节点的指针引用使用额外的内存。

无随机访问

数组允许通过索引以 O(1) 的常数时间随机访问任何元素。链表需要 O(n) 的顺序遍历才能到达特定、较慢的索引。

示例:在数组中查找第 500 个元素是瞬时的。在链表中查找第 500 个节点需要先遍历 499 个节点。

引用局部性

数组在内存中连续存储数据,允许更好的 CPU 缓存和预取利用。链表节点是动态分配的,这会损害局部性。

示例:遍历数组会将连续的内存地址加载到缓存中。链表节点的访问更随机。

顺序访问

顺序访问链表中的元素需要指针跳转。GPU 等硬件优化偏向于顺序数组访问以实现并行性和矢量化。

示例:由于顺序访问和规则结构,矩阵运算使用数组进行加速。

操作的复杂性

与等效的数组操作相比,访问、插入和删除元素等基本操作对于链表来说可能更复杂、更慢。

示例:对于链表,查找元素需要线性搜索,而对于数组,则瞬时完成。

排序困难

链表不适合排序工作负载。与对连续数组进行排序相比,指针引用使得快速排序等常用排序算法效率低下。

示例:链表归并排序需要重构节点指针。数组快速排序可以在原地重新排列元素。

固定数组的优势

数组允许随机访问、数据局部性、顺序访问和复杂性优势。一些用例大量利用了这些优点,而链表则不适合。

示例:图形管道、矩阵代数和数据库针对数组的优势进行了优化。

内存浪费

可变链表不允许避免分配未使用的节点。固定数组可以预先分配所需的全部内存。

示例:处理 1000 个元素的程序将承担分配 1000 个链表节点的开销,即使它们未使用。1000 个元素的数组可以避免这种情况。

未针对现代硬件进行优化

SIMD 指令和矢量化等趋势利用了并行性和流水线处理,在顺序数组访问上表现更好。链表更随机的内存访问无法利用这些硬件优化。

因此,对于需要大量索引、排序、顺序访问和固定内存使用的性能关键代码,应避免使用链表。然而,当主要重点是动态性和高效的插入/删除时,链表占主导地位。存在权衡,理解链表的缺点有助于明智地选择数据结构。


下一主题迷宫中的滚球