"有序链表 + 多级索引 = 跳表的魔力!" ⚡
📖 一、什么是跳表?从高速公路说起
1.1 生活中的场景
想象你要从北京开车到上海:
方案1:普通道路(单链表)
北京 → 天津 → 济南 → 徐州 → 南京 → 上海
每个城市都要经过:
- 经过100个城市
- 时间:10小时 😰
- 复杂度:O(n)
方案2:高速公路+普通道路(跳表)
顶层(高速): 北京 ========> 上海
↓ ↓
二层(国道): 北京 ---> 南京 ---> 上海
↓ ↓ ↓
底层(市道): 北京→天津→...→南京→...→上海
查找路线:
1. 从高速看:北京到上海之间
2. 下国道:找到南京附近
3. 下市道:找到具体位置
时间:1小时 ⚡
复杂度:O(log n)
1.2 专业定义
跳表(Skip List) 是一种有序链表的改进数据结构,通过维护多级索引来实现快速查找,平均时间复杂度为 O(log n)。
核心思想:
- ✅ 底层是有序链表
- ✅ 上层是多级索引(快速通道)
- ✅ 查找时从上往下,从左往右
- ⚡ 空间换时间的经典案例
为什么叫Skip?
Skip = 跳过
通过高层索引"跳过"大量节点,快速定位!
🎨 二、跳表的结构
2.1 基本结构图
4层跳表示例(查找17):
Level 4: 1 =================================> NIL
Level 3: 1 --------> 7 --------> 13 --------> NIL
↓ ↓
Level 2: 1 -> 4 ---> 7 --> 10 -> 13 --------> NIL
↓ ↓ ↓ ↓ ↓
Level 1: 1 -> 4 -> 6 -> 7 -> 10 -> 13 -> 17 -> NIL
↓ ↓ ↓ ↓ ↓ ↓ ↓
Data: [1] [4] [6] [7] [10] [13] [17]
查找17的路径:
Level 4: 1 → (17 > 1,但下一个是NIL,下降)
Level 3: 1 → 7 → 13 → (17 > 13,下降)
Level 2: 13 → (17 > 13,下降)
Level 1: 13 → 17 (找到!)
只经过了5步!比遍历链表(7步)快得多!
2.2 节点结构
跳表节点:
┌─────────────────────┐
│ value: 10 │ ← 数据值
├─────────────────────┤
│ forward[4] ───┐ │ ← Level 4的下一个节点
│ forward[3] ───┤ │ ← Level 3的下一个节点
│ forward[2] ───┤ │ ← Level 2的下一个节点
│ forward[1] ───┤ │ ← Level 1的下一个节点
└─────────────────────┘
每个节点的层数是随机的!
Java实现:
class SkipListNode {
int value;
SkipListNode[] forward; // 不同层级的next指针
public SkipListNode(int value, int level) {
this.value = value;
this.forward = new SkipListNode[level + 1];
}
}
2.3 随机层数
核心问题:新节点应该有几层?
答案:随机决定!(概率1/2)
随机算法:
Level 1: 100%的概率
Level 2: 50%的概率
Level 3: 25%的概率
Level 4: 12.5%的概率
...
代码实现:
int randomLevel() {
int level = 1;
while (Math.random() < 0.5 && level < MAX_LEVEL) {
level++;
}
return level;
}
为什么用随机?
- 简单高效
- 不需要复杂的平衡操作(不像红黑树)
- 概率上保证O(log n)
💻 三、跳表的核心操作
3.1 查找操作
算法思路:
- 从最高层开始
- 在当前层水平前进,直到下一个节点 >= 目标值
- 下降一层,重复步骤2
- 直到找到或到达底层
完整代码:
public class SkipList {
private static final int MAX_LEVEL = 16;
private static final double P = 0.5; // 概率
private SkipListNode head;
private int level; // 当前最大层数
public SkipList() {
head = new SkipListNode(Integer.MIN_VALUE, MAX_LEVEL);
level = 0;
}
// 查找
public SkipListNode search(int target) {
SkipListNode current = head;
// 从最高层开始
for (int i = level; i >= 0; i--) {
// 在当前层向右走,直到下一个节点 >= target
while (current.forward[i] != null &&
current.forward[i].value < target) {
current = current.forward[i];
}
}
// 移动到level 0的下一个节点
current = current.forward[0];
if (current != null && current.value == target) {
return current;
}
return null;
}
// 测试查找
public boolean contains(int target) {
return search(target) != null;
}
}
查找过程图解:
查找 17:
L3: 1 ──────> 7 ──────> 13 ──────> NIL
↓ ↓ ↓
L2: 1 ─> 4 ─> 7 ─> 10 ─> 13 ──────> NIL
↓ ↓ ↓ ↓ ↓
L1: 1 ─> 4 ─> 6 ─> 7 ─> 10 ─> 13 ─> 17 ─> NIL
步骤:
1. L3: start=1, next=7 (7<17),前进到7
2. L3: current=7, next=13 (13<17),前进到13
3. L3: current=13, next=NIL,下降到L2
4. L2: current=13, next=NIL,下降到L1
5. L1: current=13, next=17 (17=17),找到!✅
比较次数:5次(链表需要7次)
3.2 插入操作
算法思路:
- 找到每一层的插入位置
- 随机生成新节点的层数
- 更新每一层的指针
完整代码:
// 插入
public void insert(int value) {
SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];
SkipListNode current = head;
// 1. 找到每一层的插入位置
for (int i = level; i >= 0; i--) {
while (current.forward[i] != null &&
current.forward[i].value < value) {
current = current.forward[i];
}
update[i] = current; // 记录每层的前驱节点
}
// 2. 随机生成层数
int newLevel = randomLevel();
// 3. 如果新层数大于当前最大层数,更新
if (newLevel > level) {
for (int i = level + 1; i <= newLevel; i++) {
update[i] = head;
}
level = newLevel;
}
// 4. 创建新节点并插入
SkipListNode newNode = new SkipListNode(value, newLevel);
for (int i = 0; i <= newLevel; i++) {
newNode.forward[i] = update[i].forward[i];
update[i].forward[i] = newNode;
}
}
// 随机层数
private int randomLevel() {
int lvl = 0;
while (Math.random() < P && lvl < MAX_LEVEL) {
lvl++;
}
return lvl;
}
插入过程图解:
插入 9 (假设随机到3层):
原跳表:
L2: 1 ──────> 7 ──────> 13 ──────> NIL
↓ ↓ ↓
L1: 1 ─> 4 ─> 7 ─> 10 ─> 13 ──────> NIL
↓ ↓ ↓ ↓ ↓
L0: 1 ─> 4 ─> 7 ─> 10 ─> 13 ──────> NIL
步骤1:找到插入位置
update[2] = 7
update[1] = 7
update[0] = 7
步骤2:创建新节点(9, level=2)
步骤3:插入
L2: 1 ──────> 7 ──────> 9 ──> 13 ──> NIL
↓ ↓ ↓ ↓
L1: 1 ─> 4 ─> 7 ──────> 9 ─> 10 ─> 13 ──> NIL
↓ ↓ ↓ ↓ ↓ ↓
L0: 1 ─> 4 ─> 7 ──────> 9 ─> 10 ─> 13 ──> NIL
3.3 删除操作
算法思路:
- 找到要删除的节点及每层的前驱
- 更新每层的指针
- 如果删除后某些层为空,降低level
完整代码:
// 删除
public boolean delete(int value) {
SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];
SkipListNode current = head;
// 1. 找到每一层的前驱节点
for (int i = level; i >= 0; i--) {
while (current.forward[i] != null &&
current.forward[i].value < value) {
current = current.forward[i];
}
update[i] = current;
}
current = current.forward[0];
// 2. 如果找到了要删除的节点
if (current != null && current.value == value) {
// 3. 更新每一层的指针
for (int i = 0; i <= level; i++) {
if (update[i].forward[i] != current) {
break;
}
update[i].forward[i] = current.forward[i];
}
// 4. 降低层数(如果顶层为空)
while (level > 0 && head.forward[level] == null) {
level--;
}
return true;
}
return false;
}
🆚 四、跳表 vs 其他数据结构
4.1 跳表 vs 红黑树
| 特性 | 跳表 | 红黑树 |
|---|---|---|
| 查找 | O(log n) | O(log n) |
| 插入 | O(log n) | O(log n) |
| 删除 | O(log n) | O(log n) |
| 实现复杂度 | 简单 ⭐ | 复杂 |
| 范围查询 | 快(链表扫描)⭐ | 慢(需要遍历) |
| 并发性 | 友好(局部锁)⭐ | 困难(旋转操作) |
| 空间占用 | 稍多(多级索引) | 较少 |
| 应用 | Redis ZSet | Java TreeMap |
为什么Redis选择跳表而不是红黑树?
- 实现简单:不需要复杂的旋转操作
- 范围查询快:底层是链表,直接扫描
- 并发友好:可以对不同层级加不同的锁
4.2 跳表 vs B+树
| 特性 | 跳表 | B+树 |
|---|---|---|
| 适用场景 | 内存数据 ⭐ | 磁盘数据 |
| 节点大小 | 小(单个元素) | 大(一页数据) |
| 磁盘IO | 多 | 少 ⭐ |
| 范围查询 | 快 | 快 |
| 应用 | Redis | MySQL |
🎯 五、Redis中的跳表
5.1 ZSet的底层实现
Redis ZSet = 跳表 + 哈希表
跳表:维护有序性,支持范围查询
哈希表:O(1)查找member对应的score
数据结构:
// Redis ZSet节点
typedef struct zskiplistNode {
sds ele; // member(成员)
double score; // score(分数)
struct zskiplistNode *backward; // 后退指针
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned long span; // 跨度(用于rank)
} level[]; // 层数组
} zskiplistNode;
// Redis ZSet
typedef struct zset {
dict *dict; // 哈希表:member → score
zskiplist *zsl; // 跳表:按score排序
} zset;
5.2 Redis命令对应的操作
# 添加元素(跳表插入 + 哈希表插入)
ZADD myzset 90 Alice 85 Bob 95 Charlie
# 查询排名(跳表查找 + 计算span)
ZRANK myzset Bob # 返回1(第2名)
# 范围查询(跳表扫描)
ZRANGEBYSCORE myzset 80 90 # [Bob, Alice]
# 删除元素(跳表删除 + 哈希表删除)
ZREM myzset Alice
# 查询分数(哈希表O(1)查询)
ZSCORE myzset Bob # 返回85
5.3 完整Java实现
public class SkipList {
private static final int MAX_LEVEL = 32;
private static final double P = 0.25;
class Node {
int value;
Node[] forward;
Node(int value, int level) {
this.value = value;
this.forward = new Node[level + 1];
}
}
private Node head;
private int level;
public SkipList() {
head = new Node(Integer.MIN_VALUE, MAX_LEVEL);
level = 0;
}
// 查找
public boolean search(int target) {
Node current = head;
for (int i = level; i >= 0; i--) {
while (current.forward[i] != null &&
current.forward[i].value < target) {
current = current.forward[i];
}
}
current = current.forward[0];
return current != null && current.value == target;
}
// 插入
public void insert(int value) {
Node[] update = new Node[MAX_LEVEL + 1];
Node current = head;
for (int i = level; i >= 0; i--) {
while (current.forward[i] != null &&
current.forward[i].value < value) {
current = current.forward[i];
}
update[i] = current;
}
int newLevel = randomLevel();
if (newLevel > level) {
for (int i = level + 1; i <= newLevel; i++) {
update[i] = head;
}
level = newLevel;
}
Node newNode = new Node(value, newLevel);
for (int i = 0; i <= newLevel; i++) {
newNode.forward[i] = update[i].forward[i];
update[i].forward[i] = newNode;
}
}
// 删除
public boolean delete(int value) {
Node[] update = new Node[MAX_LEVEL + 1];
Node current = head;
for (int i = level; i >= 0; i--) {
while (current.forward[i] != null &&
current.forward[i].value < value) {
current = current.forward[i];
}
update[i] = current;
}
current = current.forward[0];
if (current != null && current.value == value) {
for (int i = 0; i <= level; i++) {
if (update[i].forward[i] != current) break;
update[i].forward[i] = current.forward[i];
}
while (level > 0 && head.forward[level] == null) {
level--;
}
return true;
}
return false;
}
// 范围查询
public List<Integer> range(int start, int end) {
List<Integer> result = new ArrayList<>();
Node current = head;
// 找到start位置
for (int i = level; i >= 0; i--) {
while (current.forward[i] != null &&
current.forward[i].value < start) {
current = current.forward[i];
}
}
current = current.forward[0];
// 收集[start, end]范围的值
while (current != null && current.value <= end) {
result.add(current.value);
current = current.forward[0];
}
return result;
}
// 随机层数
private int randomLevel() {
int lvl = 0;
while (Math.random() < P && lvl < MAX_LEVEL) {
lvl++;
}
return lvl;
}
// 打印跳表
public void display() {
for (int i = level; i >= 0; i--) {
Node node = head.forward[i];
System.out.print("Level " + i + ": ");
while (node != null) {
System.out.print(node.value + " ");
node = node.forward[i];
}
System.out.println();
}
}
// 测试
public static void main(String[] args) {
SkipList skipList = new SkipList();
// 插入数据
int[] values = {3, 6, 7, 9, 12, 17, 19, 21, 25};
System.out.println("=== 插入数据 ===");
for (int val : values) {
skipList.insert(val);
}
// 显示跳表
System.out.println("\n=== 跳表结构 ===");
skipList.display();
// 查找
System.out.println("\n=== 查找测试 ===");
System.out.println("查找17: " + skipList.search(17));
System.out.println("查找15: " + skipList.search(15));
// 范围查询
System.out.println("\n=== 范围查询 ===");
System.out.println("范围[7, 19]: " + skipList.range(7, 19));
// 删除
System.out.println("\n=== 删除9 ===");
skipList.delete(9);
skipList.display();
}
}
🎓 六、经典面试题
面试题1:跳表的时间和空间复杂度?
答案:
- 时间复杂度:
- 查找、插入、删除:O(log n) 平均,O(n) 最坏
- 空间复杂度:O(n)
- 期望:每个节点平均层数 = 1/(1-p) = 2(p=0.5时)
- 总节点数 ≈ 2n
面试题2:为什么跳表用随机层数而不是严格平衡?
答案:
- 简单:不需要复杂的平衡操作(旋转、重新着色)
- 高效:插入删除不需要全局调整
- 概率保证:随机算法期望性能是O(log n)
- 并发友好:局部修改,容易加锁
面试题3:跳表和红黑树的选择?
答案:
选择跳表的场景:
- 需要范围查询
- 并发环境
- 内存数据结构
- 实现简单优先
选择红黑树的场景:
- 空间敏感
- 确定性性能要求
- 不需要范围查询
面试题4:Redis为什么用跳表实现ZSet?
答案:
- 范围查询快:ZRANGE等命令高效
- 实现简单:代码易维护
- 并发友好:可以分段加锁
- 内存效率:相比B+树更适合内存
面试题5:如何实现并发安全的跳表?
答案:
// 方案1:全局锁(简单但性能差)
public synchronized void insert(int value) {
// ...
}
// 方案2:分段锁(性能好)
// 不同层级、不同范围使用不同的锁
// 方案3:CAS无锁(最复杂,性能最好)
// 使用原子操作更新指针
🎪 七、趣味小故事
故事:高速公路的诞生
从前,有个国家只有一条普通公路连接所有城市。
没有跳表的日子:
老王要从城市A到城市Z:
A → B → C → D → ... → Z
问题:
- 经过25个城市
- 每个城市都要停车检查
- 时间:5小时 😰
交通部长的创新(跳表):
聪明的部长小李想了个办法:
建设三层道路系统:
顶层(高速): A ========> Z
↓ ↓
中层(国道): A ---> M ---> Z
↓ ↓ ↓
底层(市道): A→B→C→...→M→...→Z
规则:
1. 先走高速,能跳就跳
2. 高速不通就下国道
3. 国道不通就下市道
老王再次出发:
1. 上高速:A → (Z太远,下高速)
2. 上国道:A → M → (Z近了,下国道)
3. 上市道:M → ... → Z
只停了3次!
时间:30分钟 ⚡
随机修路策略:
有人问:"为什么不是每个城市都有高速入口?"
小李答:"成本太高!我们随机选择:
- 100%的城市有市道
- 50%的城市有国道
- 25%的城市有高速
这样既省钱,又能保证平均速度!"
这就是跳表的智慧——概率性的多级索引!🎯
📚 八、知识点总结
核心要点 ✨
- 定义:有序链表 + 多级索引
- 核心思想:空间换时间
- 层数:随机生成(概率1/2)
- 时间复杂度:O(log n) 期望
- 应用:Redis ZSet
记忆口诀 🎵
跳表好似高速路,
多层索引帮你跳。
底层链表存数据,
上层索引当快道。
查找插入对数快,
随机层数是诀窍。
Redis用它做ZSet,
范围查询效率高!
对比总结 📊
| 数据结构 | 查找 | 插入 | 删除 | 范围查询 | 并发 |
|---|---|---|---|---|---|
| 数组 | O(log n) | O(n) | O(n) | O(k) | ❌ |
| 链表 | O(n) | O(1) | O(1) | O(n+k) | ❌ |
| 红黑树 | O(log n) | O(log n) | O(log n) | O(log n+k) | ❌ |
| 跳表 | O(log n) | O(log n) | O(log n) | O(log n+k) ⭐ | ✅⭐ |
🌟 九、总结彩蛋
恭喜你!🎉 你已经掌握了跳表这个优雅的数据结构!
记住:
- 🛣️ 有序链表 + 多级索引
- 🎲 随机层数保证性能
- ⚡ O(log n)的快速操作
- 🔴 Redis ZSet的核心
最后送你一张图
L3: ═══════════>
L2: ═══╗═══════>
L1: ═╗═╬═╗═════>
L0: ═╬═╬═╬═╬═╬=>
↓ ↓ ↓ ↓ ↓
数据链表
层层跳跃,步步登高!
下次见,继续加油! 💪😄
📖 参考资料
- Skip Lists: A Probabilistic Alternative to Balanced Trees (原始论文)
- Redis设计与实现 - 第5章
- 《算法(第4版)》- 跳表
- Redis源码 - zskiplist
作者: AI算法导师
最后更新: 2025年11月
难度等级: ⭐⭐⭐⭐ (中高级)
预计学习时间: 3-4小时
💡 温馨提示:跳表是理解Redis ZSet的关键,建议结合Redis命令一起学习!