"如果说链表是走路,那么跳表就是坐电梯!" 🛗
😮 什么是跳表?一个快递小哥的故事
📦 传统链表的烦恼
想象你是一个快递小哥,要给100号楼送快递。如果你只能从1号楼开始,一栋一栋地找:
1号 → 2号 → 3号 → ... → 98号 → 99号 → 100号 ✅
要走100步!太累了!😫
🚀 跳表的聪明之处
现在,如果小区规划了快速通道:
- 1层(基础层):每栋楼都有,1→2→3→...→100
- 2层(快速层):每10栋楼一个标记,1→10→20→30→...→100
- 3层(超快层):每50栋楼一个标记,1→50→100
第3层: 1 ─────────────────────────→ 50 ───────→ 100
↓ ↓ ↓
第2层: 1 ────→ 10 → 20 → 30 → 40 → 50 → 60 ... → 100
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
第1层: 1 → 2 → 10 ... 48 → 49 → 50 → 51 ... 98 → 100
找100号楼的路径:
- 从第3层:1 → 50 → 100 ✅(只用3步!)
找65号楼的路径:
- 第3层:1 → 50(发现50 < 65 < 100,往下跳)
- 第2层:50 → 60(发现60 < 65 < 70,往下跳)
- 第1层:60 → 61 → 62 → 63 → 64 → 65 ✅
从100步优化到了几步!这就是跳表的魔法! ✨
🏗️ 跳表的结构原理
基本概念
跳表 = 多层链表 = 链表 + 二分查找
原始链表(单层):
Level 0: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → null
查找8:需要8步 😰
跳表(多层):
Level 3: 1 ─────────────────────────→ 8 → null
↓ ↓
Level 2: 1 ─────────→ 4 ─────────→ 8 → null
↓ ↓ ↓
Level 1: 1 ────→ 2 → 4 ────→ 6 → 8 → null
↓ ↓ ↓ ↓ ↓
Level 0: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → null
查找8:1 → 8(只需2步!)⚡
节点结构
每个节点不仅有next指针,还有多层指针:
class SkipListNode {
int value; // 数据
SkipListNode[] forward; // 多层指针数组
public SkipListNode(int value, int level) {
this.value = value;
this.forward = new SkipListNode[level + 1];
}
}
图解节点:
节点3有3层:
┌─────────┐
│ value:3 │
│ ─────── │
│ Level 2 │───→ 指向更远的节点
│ Level 1 │───→ 指向中等距离的节点
│ Level 0 │───→ 指向下一个节点
└─────────┘
🎲 跳表的核心:随机层数
为什么要随机?
跳表的精妙之处在于:不需要像平衡树一样复杂的旋转操作,用随机化就能达到平衡!
// 抛硬币决定层数 🪙
private int randomLevel() {
int level = 0;
while (Math.random() < 0.5 && level < MAX_LEVEL) {
level++;
}
return level;
}
想象一下抛硬币:
- 抛一次正面 → 层数+1
- 抛一次反面 → 停止
抛硬币结果:正 正 反 → 层数 = 2
抛硬币结果:反 → 层数 = 0
抛硬币结果:正 正 正 正 反 → 层数 = 4
统计规律(概率为0.5):
- 50% 的节点只在第0层
- 25% 的节点在第0、1层
- 12.5% 的节点在第0、1、2层
- ...
这样就自然形成了金字塔结构!🏔️
Level 3: ○ ───────────────────→ ○ → null (最少节点)
↓ ↓
Level 2: ○ ────→ ○ ────→ ○ ───→ ○ → null
↓ ↓ ↓ ↓
Level 1: ○ → ○ → ○ → ○ → ○ → ○ → ○ → null
↓ ↓ ↓ ↓ ↓ ↓ ↓
Level 0: ○ → ○ → ○ → ○ → ○ → ○ → ○ → null (最多节点)
💻 跳表的Java实现
public class SkipList {
private static final int MAX_LEVEL = 16; // 最大层数
private static final double PROBABILITY = 0.5; // 晋升概率
private SkipListNode head; // 头节点
private int level; // 当前最高层数
// 节点定义
class SkipListNode {
int value;
SkipListNode[] forward; // 多层指针
public SkipListNode(int value, int level) {
this.value = value;
this.forward = new SkipListNode[level + 1];
}
}
// 构造函数
public SkipList() {
this.head = new SkipListNode(Integer.MIN_VALUE, MAX_LEVEL);
this.level = 0;
}
// 🎲 随机生成层数
private int randomLevel() {
int lvl = 0;
while (Math.random() < PROBABILITY && lvl < MAX_LEVEL) {
lvl++;
}
return lvl;
}
// 🔍 查找元素
public boolean 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];
}
}
// 移动到最底层的下一个节点
current = current.forward[0];
// 检查是否找到
return current != null && current.value == target;
}
// ➕ 插入元素
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);
// 5. 插入每一层
for (int i = 0; i <= newLevel; i++) {
newNode.forward[i] = update[i].forward[i];
update[i].forward[i] = newNode;
}
System.out.println("✅ 插入 " + value + ",层数: " + newLevel);
}
// ❌ 删除元素
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) {
for (int i = 0; i <= level; i++) {
if (update[i].forward[i] != current) {
break;
}
update[i].forward[i] = current.forward[i];
}
// 3. 更新最高层
while (level > 0 && head.forward[level] == null) {
level--;
}
System.out.println("🗑️ 删除 " + value);
return true;
}
return false;
}
// 🖨️ 打印跳表
public void printSkipList() {
System.out.println("\n━━━━━━ 跳表结构 ━━━━━━");
for (int i = level; i >= 0; i--) {
System.out.print("Level " + i + ": ");
SkipListNode node = head.forward[i];
while (node != null) {
System.out.print(node.value + " → ");
node = node.forward[i];
}
System.out.println("null");
}
System.out.println("━━━━━━━━━━━━━━━━━━━━\n");
}
}
// 测试代码
public class Main {
public static void main(String[] args) {
SkipList skipList = new SkipList();
// 插入元素
int[] values = {3, 6, 7, 9, 12, 19, 17, 26, 21, 25};
for (int val : values) {
skipList.insert(val);
}
skipList.printSkipList();
// 查找
System.out.println("查找 19: " + skipList.search(19)); // true
System.out.println("查找 15: " + skipList.search(15)); // false
// 删除
skipList.delete(19);
skipList.printSkipList();
}
}
🎬 跳表操作动画演示
查找过程
查找 19:
Step 1: 从最高层开始
Level 2: 3 ───────────→ 12 ─────→ 21 → null
↑ 当前位置 (12 < 19 < 21,往下跳)
Step 2: 在Level 1继续
Level 1: 3 ────→ 7 → 12 → 17 → 21 → null
↑ (17 < 19 < 21,往下跳)
Step 3: 在Level 0找到
Level 0: 3 → 6 → 7 → 9 → 12 → 17 → 19 → 21 → null
↑ 找到了!✅
插入过程
插入 15(假设随机层数为2):
Step 1: 找到插入位置
Level 2: 3 ──────→ 12 ─────→ 21 (12 < 15 < 21)
Level 1: 3 → 7 → 12 → 17 → 21 (12 < 15 < 17)
Level 0: 3 → ... → 12 → 17 → ... (12 < 15 < 17)
Step 2: 插入新节点15(层数2)
Level 2: 3 ──────→ 12 → 15 ──→ 21
Level 1: 3 → 7 → 12 → 15 → 17 → 21
Level 0: 3 → ... → 12 → 15 → 17 → ...
📊 跳表的性能分析
时间复杂度
| 操作 | 平均 | 最坏 | 说明 |
|---|---|---|---|
| 查找 | O(log n) ⚡ | O(n) | 类似二分查找 |
| 插入 | O(log n) ⚡ | O(n) | 查找位置 + 插入 |
| 删除 | O(log n) ⚡ | O(n) | 查找位置 + 删除 |
为什么是 O(log n)?
假设有 n 个元素,每层晋升概率是 1/2:
- Level 0:n 个节点
- Level 1:n/2 个节点
- Level 2:n/4 个节点
- Level k:n/(2^k) 个节点
总层数约为 log₂n,每层最多遍历2个节点(期望值),所以时间复杂度是 O(log n)!
空间复杂度
O(n) - 虽然有多层,但统计上每个节点平均只在2层
期望的总指针数 = n × (1/2 + 1/4 + 1/8 + ...) = n × 1 = n
所以空间复杂度还是 O(n)
🆚 跳表 vs 平衡树(红黑树/AVL树)
| 对比项 | 跳表 🎯 | 红黑树 🌲 |
|---|---|---|
| 实现难度 | 😊 简单 | 😰 复杂(旋转操作) |
| 查找 | O(log n) | O(log n) |
| 插入 | O(log n) | O(log n) |
| 删除 | O(log n) | O(log n) |
| 并发 | 😊 容易实现 | 😰 复杂 |
| 空间 | 稍多 | 少一些 |
| 范围查询 | ⚡ 快(有序) | ⚡ 快 |
跳表的优势:
- ✅ 实现简单,不需要复杂的旋转
- ✅ 并发友好,可以局部加锁
- ✅ 范围查询友好(底层是有序链表)
为什么Redis选择跳表而不是红黑树?
- 实现简单 - 代码量少,维护成本低
- 并发友好 - Redis是单线程,但未来可能多线程
- 范围查询 - ZSet的ZRANGE命令需要范围查询
🚀 Redis中的跳表应用
ZSet(有序集合)的底层实现
Redis的ZSet(Sorted Set)底层就是用跳表 + 哈希表实现的!
ZSet = Skip List + Hash Table
跳表:维护元素的有序性,支持范围查询
哈希表:快速查找元素的分数
例子:游戏排行榜 🏆
// Redis命令
ZADD leaderboard 1000 "Player1"
ZADD leaderboard 1500 "Player2"
ZADD leaderboard 1200 "Player3"
// 获取排名前10
ZRANGE leaderboard 0 9 WITHSCORES
// 获取某个分数范围的玩家
ZRANGEBYSCORE leaderboard 1000 1500
底层跳表结构:
Score 分数(升序):
Level 2: 1000 ─────────→ 1500 → null
↓ ↓
Level 1: 1000 ────→ 1200 → 1500 → null
↓ ↓ ↓
Level 0: 1000 → 1200 → 1500 → null
Player1 Player3 Player2
🎯 跳表的应用场景
✅ 适合使用跳表的场景
-
需要有序性 + 快速查找
- 游戏排行榜 🏆
- 成绩排名 📊
-
范围查询频繁
// 查询分数在[60, 80]之间的学生 findRange(60, 80); -
并发环境
- 可以对不同区域加不同的锁 🔒
-
不想写复杂的平衡树
- 跳表代码简单得多!😊
❌ 不适合使用跳表的场景
- 不需要有序性 - 用哈希表就好
- 内存极度敏感 - 跳表有额外的指针开销
- 随机访问 - 数组更好
💡 经典面试题
1. 为什么Redis用跳表而不是红黑树?
答案:
- 实现简单 - 跳表代码量远少于红黑树
- 范围查询友好 - ZSet的ZRANGE命令很常用
- 并发友好 - 跳表的局部性更好
- 内存占用相当 - 实际测试差不多
2. 跳表的随机层数为什么用1/2概率?
答案:
- 1/2是理论最优值,既保证了查找效率,又不会让层数过多
- 如果概率太大(如0.9),会有太多高层,浪费空间
- 如果概率太小(如0.1),高层太少,查找退化
3. 跳表的时间复杂度如何分析?
答案:
- 层数期望为 log n
- 每层期望遍历 1/p 个节点(p=0.5时为2个)
- 总时间:O(log n) × O(1/p) = O(log n)
📝 总结
🎓 记忆口诀
跳表就是多层链表,
随机层数保平衡。
查找插删O(log n),
Redis用它做排行榜。
抛硬币决定层数高,
正面继续反面停。
代码简单易实现,
胜过红黑树复杂!
核心要点
| 特性 | 说明 | 符号 |
|---|---|---|
| 本质 | 多层链表 | 🏢 |
| 平衡 | 随机化(不需要旋转) | 🎲 |
| 查找 | O(log n) | 🔍 |
| 插入 | O(log n) | ➕ |
| 删除 | O(log n) | ➖ |
| 空间 | O(n) | 💾 |
| 应用 | Redis ZSet | 🎯 |
🚀 下一步学习
掌握了跳表,接下来可以学习:
- 红黑树 - 另一种平衡查找树 🌲
- B+树 - MySQL索引的实现 🗂️
- 哈希表 - 另一种快速查找结构 #️⃣
恭喜你!🎉 你已经掌握了Redis的秘密武器——跳表!
记住:跳表 = 链表开了挂 = 给链表加了高速公路! 🛣️
简单、高效、优雅,这就是跳表的魅力!💪
📌 小练习:尝试实现一个简化版的跳表,支持插入和查找操作!
🤔 思考题:如果把晋升概率从0.5改成0.25,会有什么影响?
(提示:层数会变多,但每层的节点会更少,空间会节省,但查找可能稍慢)