1.一句话理解跳表
跳表 = 有序链表 + 多层索引。
底层是一条完整的有序单链表,上面又盖了几层”稀疏的索引”。查找时从最高层开始,能跳就跳,跳不动了再往下一层,平均时间复杂度 O(log n)。它用”以空间换时间”的思路,把一个慢查询的链表变成了接近二分查找的速度。
2.为什么需要跳表
先看一个朴素的对比:
| 结构 |
查找 |
插入 |
删除 |
备注 |
| 有序数组 |
O(log n)(二分) |
O(n)(搬数据) |
O(n) |
插入删除太慢 |
| 有序链表 |
O(n) |
O(1)(找到后) |
O(1)(找到后) |
查找太慢 |
| 平衡二叉搜索树 |
O(log n) |
O(log n) |
O(log n) |
实现复杂,易写错 |
| 跳表 |
O(log n) |
O(log n) |
O(log n) |
实现简单,范围查询友好 |
有序链表插入删除快,但查找要从头遍历。跳表的思路是:既然从头遍历慢,那就每隔几个结点建一条”快车道”,上层快车道更稀疏、跨度更大,查找时先走快车道,逼近目标后再往下层细找。
3.结构是多层索引
想象一条有序链表:
1
| 3 -> 6 -> 7 -> 9 -> 12 -> 17 -> 19 -> 21 -> 25 -> 26
|
我们在部分结点上”抽头”建索引(第 1 层):
1 2
| 第1层: 7 --------> 17 ------------> 25 底层 : 3 -> 6 -> 7 -> 9 -> 12 -> 17 -> 19 -> 21 -> 25 -> 26
|
查找 19:在第 1 层从 7 跳到 17,发现 17 的下一个 25 已经大于 19,于是下到底层从 17 往后走,一步就到 19。
每层索引的结点数量约为下一层的一半,所以层数约为 log n,查找步数也约为 log n。
关键问题:哪些结点该进索引? 跳表用随机化的方式——每个新结点”抛硬币”,正面就升一层,直到反面或到达上限。这样索引分布是概率均衡的,不需要像平衡树那样做旋转调整。
4.三个核心操作
三个操作的主干完全一样,就是查找:先自顶向下找到目标在每一层的前驱,只是”找到了之后做什么”不同。这个”找前驱”被抽成了一个 findPrev(target) 方法,返回一个 prev[] 数组,其中 prev[i] 是第 i 层里”目标应该插在它后面”的那个结点。
查找
从 MAX_LEVEL 层出发,当前层向右走(只要下一个结点的值 < 目标),走不动了就下到下一层,重复直到最底层。最后看第 0 层 prev[0].next[0] 的值是否等于目标。
插入
findPrev(num) 拿到 prev[]。
- 抛硬币得到新结点的层数
lvl。
- 像普通链表插入一样,把新结点挂到
0..lvl 每一层里(前驱就是 prev[i])。
删除
- 同样先
findPrev(num) 拿到 prev[]。
- 看
prev[0].next[0] 是否等于目标:不是就说明不存在,直接返回 false。
- 是的话,把它从每一层摘掉;遇到某一层
prev[i].next[i] 不再指向它(说明它压根没那一层),停止即可。
5.Java简洁实现
下面是可直接编译运行的版本,三个操作共用 findPrev。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| import java.util.Random;
public class Skiplist {
private static final int MAX_LEVEL = 16;
private final Node head = new Node(-1, MAX_LEVEL); private final Random random = new Random();
private static class Node { int val; Node[] next;
Node(int val, int level) { this.val = val; this.next = new Node[level + 1]; } }
public Skiplist() {}
private int randomLevel() { int level = 0; while (random.nextDouble() < 0.5 && level < MAX_LEVEL) { level++; } return level; }
private Node[] findPrev(int target) { Node[] prev = new Node[MAX_LEVEL + 1]; Node curr = head; for (int i = MAX_LEVEL; i >= 0; i--) { while (curr.next[i] != null && curr.next[i].val < target) { curr = curr.next[i]; } prev[i] = curr; } return prev; }
public boolean search(int target) { Node[] prev = findPrev(target); Node node = prev[0].next[0]; return node != null && node.val == target; }
public void add(int num) { Node[] prev = findPrev(num); Node node = new Node(num, randomLevel()); int top = node.next.length - 1; for (int i = 0; i <= top; i++) { node.next[i] = prev[i].next[i]; prev[i].next[i] = node; } }
public boolean erase(int num) { Node[] prev = findPrev(num); Node node = prev[0].next[0]; if (node == null || node.val != num) { return false; } for (int i = 0; i <= MAX_LEVEL; i++) { if (prev[i].next[i] != node) { break; } prev[i].next[i] = node.next[i]; } return true; }
public static void main(String[] args) { Skiplist sl = new Skiplist(); sl.add(1); sl.add(2); sl.add(3); System.out.println("search(0) = " + sl.search(0)); sl.add(4); System.out.println("search(1) = " + sl.search(1)); System.out.println("erase(0) = " + sl.erase(0)); System.out.println("erase(1) = " + sl.erase(1)); System.out.println("search(1) after erase = " + sl.search(1)); } }
|
跑一遍 main 的输出:
1 2 3 4 5
| search(0) = false search(1) = true erase(0) = false erase(1) = true search(1) after erase = false
|
6.时间复杂度
- 查找 / 插入 / 删除:平均
O(log n),最坏 O(n)(极端情况下所有结点都堆在同一层)。
- 空间:平均
O(n)(每个结点期望层数约为 2,索引额外占用约 n 个指针)。
- 概率保证下,层数超过
3 log n 的概率极小,MAX_LEVEL = 16 足以支撑千万级数据。
7.跳表 vs 红黑树
Redis 的 ZSet 用跳表而不是红黑树,主要因为:
- 范围查询友好:跳表找到起点后,沿底层链表顺序遍历即可拿到一段区间;红黑树需要中序遍历,缓存局部性差。
- 实现简单、不易写错:跳表没有旋转、变色等平衡操作,代码量小一半以上(上面这版只有 70 行左右)。
- 性能相当:两者增删查都是
O(log n),跳表在范围操作上更自然。
代价是跳表空间占用略高(多存索引指针),以及最坏情况退化为 O(n)——但概率极低,工程中可接受。
8.小结
- 跳表 = 有序链表 + 随机多层索引,平均
O(log n)。
- 核心难点在
prev[] 数组:插入 / 删除 / 查找前先自顶向下记录每层前驱,抽成 findPrev 后三个操作都很短。
- 层数靠抛硬币随机,天然平衡,不需要旋转。
- 一句话:跳表用概率换实现简单,用空间换查询快,最适合带范围查询的有序集合。
end……