跳表(Skip List)是一种概率型数据结构,由 William Pugh 于 1989 年提出。它通过"多级索引"在有序链表之上建立快速查找路径,从而在期望意义上达到与平衡树相近的 O(log n) 查找、插入和删除复杂度。跳表的最大特点是:实现简单、支持范围查询、天然适合并发场景。
本文将从原理、复杂度、使用场景、实际应用,以及 Java / Python 代码示例几个方面完整介绍跳表。
一、跳表的核心思想
普通有序链表查找一个元素需要从头遍历,时间复杂度是 O(n)。
例如:
text
head -> 3 -> 6 -> 7 -> 9 -> 12 -> 17 -> 19 -> 21 -> 25 -> 26 -> null
如果要找 25,必须从头一个个比较。
跳表的做法是:在原始链表之上建立多级索引。高层索引跨度大,用于快速跳跃;底层链表保存全部元素,用于精确查找。
例如:
text
Level 2: head -------------------------> 21 -> null
Level 1: head -------> 7 -> 12 -------> 21 -> null
Level 0: head -> 3 -> 6 -> 7 -> 9 -> 12 -> 17 -> 19 -> 21 -> 25 -> 26 -> null
查找 25 时:
- 从最高层
Level 2开始,向右走到21; - 发现下一个是
null,下降到Level 1; 21后面没有更合适的节点,下降到Level 0;- 从
21继续向右,找到25。
这样跳过了大量节点。
二、跳表的结构
跳表由节点组成,每个节点包含:
value:存储的值;forward[]:指向不同层级的下一个节点的指针数组。
例如一个节点的 forward 长度为 4,表示它出现在第 0、1、2、3 层。
text
Node:
value = 12
forward[0] -> 17
forward[1] -> 19
forward[2] -> null
跳表还有一个头节点 head,它通常拥有最大层数,但不存储实际数据。
三、跳表的核心操作
1. 查找
从最高层开始:
- 如果当前节点的下一个节点值小于目标值,就向右移动;
- 否则下降到下一层;
- 到达第 0 层后,检查下一个节点是否等于目标值。
伪代码:
text
cur = head
for level from maxLevel down to 0:
while cur.forward[level] != null and cur.forward[level].value < target:
cur = cur.forward[level]
cur = cur.forward[0]
return cur != null and cur.value == target
2. 插入
插入时先查找每一层的前驱节点,记录到 update[] 数组中。然后随机生成新节点的层数,最后逐层插入。
关键点:新节点的层数由随机函数决定。
例如:
text
while random() < p and level < MAX_LEVEL:
level++
通常 p = 0.5 或 p = 0.25。p 越小,高层节点越少,空间越省,但查找稍慢。
3. 删除
删除时同样先找到每一层的前驱节点,然后逐层断开指针。
如果删除后最高层为空,就降低跳表当前最大层数。
四、复杂度分析
假设晋升概率为 p,节点总数为 n。
| 操作 | 期望复杂度 | 最坏复杂度 |
|---|---|---|
| 查找 | O(log n) |
O(n) |
| 插入 | O(log n) |
O(n) |
| 删除 | O(log n) |
O(n) |
| 空间 | O(n) |
O(n log n) |
说明:
- 期望层数约为
log_{1/p} n; - 每一层期望访问
1/p个节点; - 因此查找总步数期望为
O(log n); - 每个节点的期望层数为
1 / (1 - p),所以总指针数期望为O(n); - 最坏情况理论上可能退化成普通链表,但概率极低。
例如 p = 0.5,100 万个元素期望层数约为 log2(1,000,000) ≈ 20。所以 MAX_LEVEL = 32 通常足够。
五、跳表 vs 平衡树 vs 哈希表
| 特性 | 跳表 | 平衡树 | 哈希表 |
|---|---|---|---|
| 平均查找 | O(log n) |
O(log n) |
O(1) |
| 最坏查找 | O(n),概率极低 |
O(log n) |
O(n) |
| 是否有序 | 是 | 是 | 否 |
| 范围查询 | 非常自然 | 支持 | 不支持 |
| 实现难度 | 简单 | 较复杂 | 简单 |
| 并发实现 | 较容易 | 较难 | 扩容较难 |
| 内存开销 | 多级指针 | 平衡因子/颜色 | 桶 + 链表/树 |
跳表的优势:
- 不需要旋转,不需要复杂平衡逻辑;
- 范围查询、排序遍历非常方便;
- 并发插入删除更容易实现;
- 代码量通常远少于红黑树、AVL 树。
跳表的劣势:
- 查找是期望
O(log n),不是严格最坏O(log n); - 指针较多,缓存局部性可能不如 B+ 树;
- 磁盘存储场景通常还是 B+ 树更合适。
六、使用场景
跳表适合以下场景:
-
有序集合 / 有序映射
- 需要按 key 排序;
- 需要范围查询,如
[start, end]; - 需要排名、Top N、前后驱查询。
-
排行榜
- 按分数排序;
- 查询某个用户排名;
- 查询前 100 名;
- 查询分数区间内的用户。
-
延时队列
- 按到期时间排序;
- 快速取出最早到期的任务。
-
内存数据库索引
- 数据在内存中,需要有序结构;
- 写入和范围扫描都很频繁。
-
并发有序结构
- 多线程同时读写;
- 不希望使用全局锁;
- Java 的
ConcurrentSkipListMap就是典型代表。
-
LSM-Tree 的 MemTable
- 写入先进入内存跳表;
- 跳表保持有序;
- 后续 flush 成 SSTable。
七、实际应用
1. Redis 有序集合 ZSET
Redis 的 ZSET 是最著名的跳表应用之一。
ZSET 同时使用:
- 哈希表:
member -> score,实现O(1)查分数; - 跳表:按
score排序,支持范围查询和排名。
Redis 跳表特点:
- 最大层数
ZSKIPLIST_MAXLEVEL = 32; - 晋升概率
ZSKIPLIST_P = 0.25; - 每个节点有
backward指针,支持逆序遍历; - 每个层级指针有
span,用于计算排名ZRANK。
常见命令:
text
ZADD ranking 100 user1
ZADD ranking 200 user2
ZRANGE ranking 0 -1 WITHSCORES
ZRANK ranking user1
ZRANGEBYSCORE ranking 100 200
这些操作背后都依赖跳表。
2. Java ConcurrentSkipListMap / ConcurrentSkipListSet
Java 标准库提供了:
ConcurrentSkipListMapConcurrentSkipListSet
它们是线程安全的有序 Map / Set,基于跳表实现,支持:
- 高并发读写;
- 范围查询;
- 有序遍历;
- 弱一致性迭代器。
HBase 的 MemStore 就使用了 ConcurrentSkipListMap 来维护内存中的有序数据。
3. LevelDB / RocksDB MemTable
LevelDB 和 RocksDB 的 MemTable 使用跳表作为内存表:
- 写入先写 MemTable;
- MemTable 按 key 有序;
- 读操作可以在内存中快速查找;
- 写满后冻结并 flush 到 SSTable。
为什么用跳表而不是平衡树?
- 跳表实现简单;
- 并发写入更容易;
- 不需要旋转;
- 范围扫描自然。
4. Lucene 倒排索引
Lucene 的倒排索引中,postings list 会使用 skip list 结构加速文档号跳跃。例如多个词项求交集时,可以通过跳表指针快速跳过不可能匹配的文档。
5. 其他应用
- 排行榜系统;
- 实时风控的有序窗口;
- 时间线排序;
- 分布式系统中的有序索引;
- 内存缓存的有序淘汰策略。
八、Java 实现示例
下面是一个简化版、线程不安全的跳表实现,用于教学。
java
import java.util.Random;
public class SkipList<T extends Comparable<T>> {
private static final int MAX_LEVEL = 16;
private static final double P = 0.5;
private static class Node<T> {
T value;
Node<T>[] forward;
@SuppressWarnings("unchecked")
Node(T value, int level) {
this.value = value;
this.forward = (Node<T>[]) new Node[level + 1];
}
}
private final Node<T> head;
private int level;
private final Random random;
@SuppressWarnings("unchecked")
public SkipList() {
this.head = new Node<>(null, MAX_LEVEL);
this.level = 0;
this.random = new Random();
}
private int randomLevel() {
int lvl = 0;
while (lvl < MAX_LEVEL && random.nextDouble() < P) {
lvl++;
}
return lvl;
}
public boolean search(T target) {
Node<T> cur = head;
for (int i = level; i >= 0; i--) {
while (cur.forward[i] != null &&
cur.forward[i].value.compareTo(target) < 0) {
cur = cur.forward[i];
}
}
cur = cur.forward[0];
return cur != null && cur.value.compareTo(target) == 0;
}
@SuppressWarnings("unchecked")
public void insert(T value) {
Node<T>[] update = (Node<T>[]) new Node[MAX_LEVEL + 1];
Node<T> cur = head;
for (int i = level; i >= 0; i--) {
while (cur.forward[i] != null &&
cur.forward[i].value.compareTo(value) < 0) {
cur = cur.forward[i];
}
update[i] = cur;
}
cur = cur.forward[0];
if (cur != null && cur.value.compareTo(value) == 0) {
return; // 简化:不插入重复值
}
int newLevel = randomLevel();
if (newLevel > level) {
for (int i = level + 1; i <= newLevel; i++) {
update[i] = head;
}
level = newLevel;
}
Node<T> newNode = new Node<>(value, newLevel);
for (int i = 0; i <= newLevel; i++) {
newNode.forward[i] = update[i].forward[i];
update[i].forward[i] = newNode;
}
}
@SuppressWarnings("unchecked")
public boolean delete(T value) {
Node<T>[] update = (Node<T>[]) new Node[MAX_LEVEL + 1];
Node<T> cur = head;
for (int i = level; i >= 0; i--) {
while (cur.forward[i] != null &&
cur.forward[i].value.compareTo(value) < 0) {
cur = cur.forward[i];
}
update[i] = cur;
}
cur = cur.forward[0];
if (cur == null || cur.value.compareTo(value) != 0) {
return false;
}
for (int i = 0; i <= level; i++) {
if (update[i].forward[i] != cur) {
break;
}
update[i].forward[i] = cur.forward[i];
}
while (level > 0 && head.forward[level] == null) {
level--;
}
return true;
}
public void print() {
for (int i = level; i >= 0; i--) {
Node<T> cur = head.forward[i];
System.out.print("Level " + i + ": ");
while (cur != null) {
System.out.print(cur.value + " -> ");
cur = cur.forward[i];
}
System.out.println("null");
}
}
public static void main(String[] args) {
SkipList<Integer> skipList = new SkipList<>();
int[] nums = {3, 6, 7, 9, 12, 19, 17, 26, 21, 25};
for (int num : nums) {
skipList.insert(num);
}
skipList.print();
System.out.println("search 19: " + skipList.search(19));
System.out.println("search 15: " + skipList.search(15));
skipList.delete(19);
System.out.println("after delete 19, search 19: " + skipList.search(19));
}
}
Java 标准库示例
生产环境中,更推荐直接使用 ConcurrentSkipListMap:
java
import java.util.concurrent.ConcurrentSkipListMap;
public class ConcurrentSkipListDemo {
public static void main(String[] args) {
ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();
map.put(3, "C");
map.put(1, "A");
map.put(5, "E");
map.put(2, "B");
map.put(4, "D");
System.out.println(map.firstKey()); // 1
System.out.println(map.lastKey()); // 5
System.out.println(map.ceilingKey(3)); // 3
System.out.println(map.floorKey(3)); // 3
System.out.println(map.subMap(2, true, 4, true));
// {2=B, 3=C, 4=D}
}
}
九、Python 实现示例
Python 标准库没有内置跳表,但可以手写一个简化版本。
python
import random
class Node:
__slots__ = ("value", "forward")
def __init__(self, value=None, level=0):
self.value = value
self.forward = [None] * (level + 1)
class SkipList:
def __init__(self, max_level=16, p=0.5):
self.max_level = max_level
self.p = p
self.level = 0
self.head = Node(None, max_level)
self._random = random.Random()
def _random_level(self):
lvl = 0
while lvl < self.max_level and self._random.random() < self.p:
lvl += 1
return lvl
def search(self, target):
cur = self.head
for i in range(self.level, -1, -1):
while cur.forward[i] is not None and cur.forward[i].value < target:
cur = cur.forward[i]
cur = cur.forward[0]
return cur is not None and cur.value == target
def insert(self, value):
update = [None] * (self.max_level + 1)
cur = self.head
for i in range(self.level, -1, -1):
while cur.forward[i] is not None and cur.forward[i].value < value:
cur = cur.forward[i]
update[i] = cur
cur = cur.forward[0]
if cur is not None and cur.value == value:
return # 简化:不插入重复值
new_level = self._random_level()
if new_level > self.level:
for i in range(self.level + 1, new_level + 1):
update[i] = self.head
self.level = new_level
new_node = Node(value, new_level)
for i in range(new_level + 1):
new_node.forward[i] = update[i].forward[i]
update[i].forward[i] = new_node
def delete(self, value):
update = [None] * (self.max_level + 1)
cur = self.head
for i in range(self.level, -1, -1):
while cur.forward[i] is not None and cur.forward[i].value < value:
cur = cur.forward[i]
update[i] = cur
cur = cur.forward[0]
if cur is None or cur.value != value:
return False
for i in range(self.level + 1):
if update[i].forward[i] is not cur:
break
update[i].forward[i] = cur.forward[i]
while self.level > 0 and self.head.forward[self.level] is None:
self.level -= 1
return True
def print_all(self):
for i in range(self.level, -1, -1):
cur = self.head.forward[i]
values = []
while cur is not None:
values.append(str(cur.value))
cur = cur.forward[i]
print(f"Level {i}: " + " -> ".join(values))
if __name__ == "__main__":
sl = SkipList()
for x in [3, 6, 7, 9, 12, 19, 17, 26, 21, 25]:
sl.insert(x)
sl.print_all()
print("search 19:", sl.search(19))
print("search 15:", sl.search(15))
sl.delete(19)
print("after delete 19, search 19:", sl.search(19))
十、工程注意事项
-
随机层数使用线程安全随机源
- 并发环境下不要使用共享的普通
Random; - Java 可用
ThreadLocalRandom; - 或者直接使用
ConcurrentSkipListMap。
- 并发环境下不要使用共享的普通
-
选择合适的
p和MAX_LEVELp = 0.5:实现简单,空间约2n指针;p = 0.25:空间更省,Redis 采用;MAX_LEVEL一般取 32 或 64;- 元素规模越大,层数需求越高。
-
重复值处理
- 本文示例不允许重复;
- 如果需要重复,可以增加
count字段; - 或使用复合 key,如
(value, id)。
-
范围查询
- 先找到
>= start的第一个节点; - 然后沿第 0 层向后遍历到
> end停止; - 这是跳表非常强的能力。
- 先找到
-
排名查询
- 普通跳表不直接支持
rank; - Redis 在每个指针中维护
span,从而支持ZRANK。
- 普通跳表不直接支持
-
生产环境优先使用成熟实现
- Java:
ConcurrentSkipListMap、ConcurrentSkipListSet; - Redis:
ZSET; - LevelDB / RocksDB:MemTable;
- Python:可使用 Redis,或第三方有序容器库。
- Java:
十一、总结
跳表是一种用"概率平衡"代替"严格平衡"的数据结构。它通过在有序链表上建立多级索引,实现了期望 O(log n) 的查找、插入和删除。
它的核心优势是:
- 实现简单;
- 支持有序遍历和范围查询;
- 并发实现相对容易;
- 在内存索引、排行榜、延时队列、LSM-Tree MemTable 等场景中非常实用。
实际工程中,Redis 的 ZSET、Java 的 ConcurrentSkipListMap、LevelDB / RocksDB 的 MemTable 都是跳表的经典应用。理解跳表,不仅能掌握一种高效数据结构,也能更好理解现代存储系统和并发容器的设计思路。