目录
一、前言
跳表是一种特殊的数据结构,是一种概率性的有序数据结构,它通过多层链表的方式提供高效的查找、插入和删除操作,平均时间复杂度为O(log n)。跳表由William Pugh于1990年提出,结合了链表和二分查找的思想,是平衡树(如AVL树、红黑树)的替代方案,但实现更简单。
我们为什么需要引出跳表这样一个数据结构?
在实际开发中,高效查找是核心需求之一,而常用的支持高效查找算法的实现方式有以下几种:
-
有序数组:插入时可以先对数据排序,查询时可以采用二分查找算法降低查找操作的复杂度。缺点是插入和删除数据时,为了保持元素的有序性,需要进行大量数据的移动操作。
-
二叉查找树:既支持高效的二分查找算法,又能快速的进行插入和删除操作的数据结构,理想的时间复杂度为 O($log^n$),但是在某些极端情况下,二叉查找树有可能变成一个线性链表,即退化成链表结构。
-
平衡二叉树:基于二叉查找树的优点,对其缺点进行了优化改进,引入了平衡的概念。为了维持二叉树的平衡衍生出了多种平衡算法,根据平衡算法的不同具体实现有AVL树 /B树(B-Tree)/ B+树(B+Tree)/红黑树 等等。但是平衡算法的实现大多数比较复杂且较难理解。
而跳表与传统的数据结构的对比总结如下:
| 特性 | 有序数组 | 二叉查找树 | 平衡二叉树 | 跳表 (Skip List) |
|---|---|---|---|---|
| 查找时间复杂度 | O(log n) | 最坏O(n) | O(log n) | O(log n) 平均 |
| 插入/删除效率 | O(n) 数据迁移 | O(log n) 可能退化 | O(log n) 需平衡操作 | O(log n) 无迁移 |
| 实现复杂度 | 简单 | 中等 | 复杂 (旋转/分裂) | 简单 (无旋转) |
| 范围查询 | 高效但需数据迁移 | 需中序遍历 | 需中序遍历 | 链表遍历 O(k) |
| 内存占用 | 紧凑 | 指针开销 | 指针+平衡因子 | 额外指针 (约2倍) |
| 并发控制 | 全局锁 | 复杂锁机制 | 复杂锁机制 | 无锁实现可行 |
可以看到跳表同样支持对数据进行高效的查找,插入和删除数据操作时间复杂度能与平衡二叉树媲美,最重要的是跳表的实现比平衡二叉树简单几个级别。缺点就是“以空间换时间”方式存在一定数据冗余。
二、跳表
1、设计思想
skiplist,顾名思义,首先它是一个list。实际上,它是在有序链表的基础上发展起来的。
对于一 个有序的链表,我们知道想要查找某个数据的时候只能从头结点沿着链表依次比较查找,时间复杂度是O(N)。如下图所示

假设现在我们有个要求,想快速找到上图链表中的12 这个元素,只能从头开始遍历链表,直到找到我们需要找的元素。查找路径:1、3、6、7、9、12。这样的查找效率很低,平均时间复杂度很高O(n)。那有没有办法提高链表的查找速度呢?
链表不能像数组那样通过索引快速访问数据,只能沿着指针方向依次访问,所以不能使用二分查找算法快速进行数据查询。但是可以借鉴创建索引的这种思路,就像图书的目录一样,如果我要查看第六章的内容,直接翻到通过目录查询到的第六章对应页码处就行。
这里的目录就相当于创建的索引,该索引能够缩小我们查询数据的范围减少查询次数。在原始链表的基础上,我们增加一层索引链表,假如原始链表的每两个结点就有一个结点也在索引链表当中,如图所示:

在加了一层索引之后,我们找 12 是什么样子的呢?从起始开始,先在上面新增加的索引找 6、9、17,遍历到一级索引的 9 时,发现 9 的后继节点是 17,比 12 大,于是不往后找了,而是通过 9 找到原始链表(图中下面的链表,即使用9下面的指针)的 9,然后再往后遍历找到了我们要找的 12,遍历结束。有没有发现,加了一级索引后,查找路径:6、9、17,查找节点需要遍历的元素相对少了,我们不需要对 12 之前的所有数据都遍历,查找的效率提升了。
如果再加一层索引结构呢?那查找元素的效率就更高了,这就是跳表的思想,用“空间换时间”,通过给链表建立索引,提高了查找的效率。

我们所举的例子还只是数据很少的情况,所以在现在看来效率好像也没有提升多少,但是当数据量足够大时,效率提升会很大。假如有序单链表现在有1万个元素,分别是 0~9999。现在我们建了很多级索引,最高级的索引,就两个元素 0、5000,次高级索引四个元素 0、2500、5000、7500,依次类推,当我们查找 7890 这个元素时,查找路径为 0、5000、7500 … 7890,通过最高级索引直接跳过了5000个元素,次高层索引直接跳过了2500个元素,从而使得链表能够实现二分查找。由此可以看出,当元素数量较多时,索引提高的效率比较大,近似于二分查找。
总结:
- 假如我们每相邻两个节点升高一层,增加一个指针,让指针指向下下个节点,如上面第二个图所示。这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半。由于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了,需要比较的节点数大概 只有原来的一半。
- 以此类推,我们可以在第二层新产生的链表上,继续为每相邻的两个节点升高一层,增加一 个指针,从而产生第三层链表。如上边第三个图,这样搜索效率就进一步提高了。
- 跳表正是受这种多层链表的想法的启发而设计出来的。实际上,按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似 二分查找,使得查找的时间复杂度可以降低到O(log n)。
但是这个结构在插入删除数据的时候有很大的问题,插入或者删除一个节点之后,就会打乱上下相邻两层链表上节点个数严格 的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也 包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。
跳表的设计为了避免这种问题,做了一个大胆的处理,不再严格要求对应比例关系,而是 插入一个节点的时候随机出一个层数。这样每次插入和删除都不需要考虑其他节点的层数, 这样就好处理多了。插入一个新的数据 17 的细节过程入下图

这样做的好处就是每一个节点的插入和删除都跟其他节点是没有关系的,都是独立的互不影响的,这样就可以在不用调整其他节点层数的情况下进行新节点的插入和删除。
解释:
跳表是一种分层链表结构,通过“高层指针跳过更多节点”实现快速查找(类似二分法)。其核心设计是:
- 层级:每个节点有随机数量的“层”(由
random()生成,通常遵循几何分布,比如50%概率有1层,25%概率有2层,12.5%概率有3层,以此类推); - 搜索路径:插入/删除时,需找到每一层中最后一个比目标值小的节点(称为“前驱节点”),这些节点的指针需要更新以指向新节点;
- 随机层数:避免严格的层级比例(如传统跳表要求每层节点数是下层的1/2),简化插入/删除操作(无需调整其他节点的层数)。
上图展示了如何插入数据17的过程,上半部分是插入前的原始跳表,下半部分是插入后的跳表。
Search path:搜索17时的路径(箭头所示),即找到所有需要更新的前驱节点;update[i]→forward[i]:表示前驱节点的forward指针(下一层指针)需要从指向原节点改为指向新节点17;NIL:链表末尾的空指针。
跳表的“随机层数”设计解决了传统分层链表的层级维护难题,通过搜索路径+指针更新实现高效插入,而图片正是这一逻辑的直观呈现。插入17的过程本质是:找到前驱节点→更新指针→保持有序,所有操作均围绕“层级结构”和“随机层数”展开。


2415

被折叠的 条评论
为什么被折叠?



