C++ 中的跳表

2025 年 5 月 12 日 | 阅读 9 分钟

跳表是一种概率性数据结构,它提供了一种高效的搜索、插入和删除有序序列中元素的方法。它由 **William Pugh** 于 **1989** 年发明,作为平衡树的一种替代方案,具有相似的平均情况性能特征,但实现更简单。

问题陈述

在处理需要使用数组或链表进行搜索、插入或删除的大型数据集时,经常会出现问题。然而,它们在大多数情况下通常具有线性时间复杂度。尽管使用平衡树(如 AVL 树和红黑树)可以通过这些操作获得对数时间复杂度,但它们的实现和维护可能非常复杂。

跳表通过在数据结构中引入随机性来解决这些问题。它们使用具有不同跳跃距离的多个链表,从而实现更快的搜索,因此非常适合需要快速搜索的场景,例如数据库系统和搜索引擎。

在本文中,我们将探讨 **跳表** 的详细信息,并用 C++ 创建一个简单的版本。我们还将讨论重要的概念以及搜索、插入和删除元素的过程,并给出如何实现的示例。让我们从理解跳表是什么以及为什么它们可以被认为是数据结构中有用的工具开始。

程序

让我们以一个 C++ 程序来说明 **跳表**。

输出

Skip List
Level 0: 3 6 7 9 12 17 19 
Level 1: 7 12 17 
Searching for 9: Found
Searching for 15: Not Found
Skip List
Level 0: 3 6 7 12 17 19 
Level 1: 7 12 17 
Searching for 9: Not Found

说明

1. 节点结构

  • 跳表中的每个节点都包含一个键(此处为整数)和一个称为 **forward** 的指针数组。
  • forward 数组是根据跳表的最高级别动态分配的。

2. SkipList 类

  • 此类管理跳表的结构和操作。
  • 它包含指向其头节点的指针、一个用于跟踪跳表当前级别的整数,以及一个用作浮点数的概率因子 p,用于确定节点的级别。
  • 为了初始化头节点,将初始级别设置为 0,并为级别生成播种随机数生成器。
  • 析构函数应删除头节点及其关联的内存,以确保正确清理内存。

3. 随机级别生成

  • **randomLevel()** 函数根据概率因子 p 为新节点生成一个随机级别。
  • 它使用 **rand()** 函数生成一个介于 0 和 1 之间的随机浮点数,并将其与 p 进行比较,只要随机值大于或等于 p 或达到最高级别,就增加级别。

4. 插入操作

  • **insert(int key)** 函数将具有给定键的新节点插入到跳表结构中。
  • 遍历从 **headernode** 开始,同时更新前向指针以在各个级别中找到正确的插入位置。
  • 如果键已存在,则使用新值更新其节点;否则,它会创建另一个节点并相应地调整级别。

5. 搜索操作

  • **search(int key)** 函数在跳表中搜索一个键。
  • 它从头节点开始,逐级向下移动,同时比较键,直到找到匹配项或到达该级别的末尾。

6. 删除操作

  • **remove(int key)** 函数从跳表中删除具有指定键的节点。
  • 与插入一样,它会遍历每个级别以查找要删除的节点,修改前一个节点的 forward 指针,并为已删除的节点释放内存。

7. 显示函数

  • **display()** 函数将输出跳表的内容,显示每个级别以及其中的每个键。

8. main 函数

  • 在 **main()** 函数中,创建 **SkipList** 类的一个实例。
  • 将多个键插入到跳表中,然后演示搜索和删除,以确保跳表正常工作。

时间和空间复杂度

时间复杂度

  • 搜索操作(平均情况):O(log n),其中 n 是跳表中的元素数量。平均情况时间复杂度源于跳表的结构,该结构具有多个层以实现更快的遍历以访问元素。
  • 插入操作(平均情况):O(log n),与搜索操作类似,使用遍历多个级别来查找插入的正确位置。
  • 删除操作(平均情况):O(log n),还涉及遍历多个级别以查找要删除的节点。

注意:上述时间复杂度是平均情况场景,假设一个平衡良好的跳表具有适合级别生成的概率因子(p)。

空间复杂度

  • 跳表的空间复杂度取决于它包含的元素数量以及跳表的最高级别。
  • 对于具有 n 个元素且最多 m 个级别的跳表,空间复杂度为 **O(n * m)**,因为每个节点都具有指向其之上所有级别的指针,直到最高级别。
  • 然而,考虑到最高高度(m)通常保持较小(例如,实际中通常为 16 或更少),与其他平衡树数据结构(如 AVL 树或红黑树)相比,跳表的空间开销适中。

跳表的特点

C++ 中的 **跳表** 有几个特点。跳表的一些主要特点如下:

  1. 高效的搜索操作:跳表允许快速搜索,平均情况时间复杂度为 **O(log n)**。这种效率是通过使用多个链表级别来实现的,从而可以快速找到所需元素。
  2. 平衡结构:与搜索时间可能达到线性复杂度的简单链表不同,跳表始终保持平衡结构,这主要是由于随机级别生成。因此,这些操作的平均时间复杂度为对数级别,因此适用于大型数据集。
  3. 概率方法:在插入期间,跳表使用概率方法来确定每个节点所在的级别,从而使节点在各个级别上均匀分布。这种随机性使跳表保持平衡,并且与同类平衡树结构相比,简化了其实现。
  4. 易于实现:与复杂的平衡树结构(如 AVL 树或红黑树)相比,跳表相对简单。事实上,其插入、搜索和删除操作的逻辑都很简单,对开发人员来说普遍友好。
  5. 跳表在调整最大级别和概率因子(用于 **级别生成 (p)**)等参数方面具有灵活性。开发人员可以根据特定需求以及数据集的性能和特性来调整这些参数。
  6. 跳表在需要排序数据管理的各种应用中都有广泛的应用。例如,数据库、缓存系统、优先队列等。它们实现了简洁性、效率和可扩展性的良好结合。
  7. 跳表在搜索、插入和删除操作方面的平均时间复杂度使其在需要快速访问或修改排序数据的场合具有吸引力。

跳表的缺点

C++ 中的 **跳表** 有几个缺点。跳表的一些主要缺点如下:

  1. 空间开销更大:与数组或链表等简单数据结构相比,跳表的空间开销更大。这是因为每个节点都有指向多个级别的指针,尤其是当跳表具有较高的最大级别时。
  2. 不可预测的性质:性能的随机性源于跳表的概率行为。虽然它们的平均情况时间复杂度已得到充分表征,但实际性能取决于键的分布和级别的随机生成。
  3. 实现复杂性:尽管与 AVL 树或红黑树等平衡树相比,跳表的实现通常更简单,但它们仍然需要仔细操作指针和级别。当涉及到优化或高级功能时,这种复杂性可能会加剧。
  4. 不适用于原地操作:跳表不适合原地操作或内存利用至关重要的场景。额外的指针和级别使得原地进行更改变得困难,并增加了内存使用量。
  5. 应用有限:跳表最适用于需要高效搜索、插入和删除功能的场景,尤其是在涉及大型排序数据集时。其他数据访问模式或特殊需求可能需要其他数据结构。
  6. 性能波动:虽然跳表提供快速的平均情况性能,但其最坏情况下的时间复杂度可能高于某些其他数据结构。对于需要始终如一的可预测性能的应用,它可能不总是最佳选择。

结论

总而言之,跳表是一种适应性强且有效的概率性数据结构,其搜索、添加和删除元素的平均时间复杂度为 **O(log n)**。它们被引入作为常规平衡树结构(如红黑树和 AVL 树)的替代品,以获得相似的性能,同时更容易开发和维护。