跳表 (SkipList)
跳表是多层的链表——每层的节点指向同层的下一个节点,上层节点跨越多层,实现”跳跃”查找。
1 | 跳表结构 (Redis ZSet 实现): |
查找过程 (找 key=21): 从最高层 Level 3 开始 → 30 > 21,退到 Level 2 → 15 < 21 → 跳到 15 → 30 > 21,退到 Level 1 → 从 15 走到 21 → 命中!整个过程跳过了 Level 0 的多个节点,复杂度 O(log n)。
时间复杂度分析:
- 查找:O(log n)(概率期望,与红黑树相同)
- 插入:O(log n)(随机生成层数,期望 O(log n))
- 删除:O(log n)
- 空间复杂度:O(n)(每层节点的期望比例为 1/2,总节点数 ≈ 2n)
Redis ZSet 为什么选跳表而不是红黑树?
| 对比维度 | 跳表 | 红黑树 |
|---|---|---|
| 实现复杂度 | 简单(300-400 行 C) | 复杂(旋转/变色逻辑繁琐) |
| 范围查询 | O(log n + k),天然支持链表遍历 | 需要中序遍历(递归/栈) |
| 并发支持 | 容易实现无锁(每层独立的链表) | 几乎不可能无锁(旋转影响多个节点) |
| 内存占用 | 略大(多指针) | 较小(两个子节点指针 + 颜色) |
| 查询性能 | 稍慢(概率均衡,常数因子更大) | 稍快(确定性平衡) |
Redis 的 ZSet 需要频繁做 ZRANGEBYSCORE 范围查询——跳表的底层 Level 0 就是一条完整的有序链表,直接遍历即可,而红黑树需要中序遍历做栈或递归。此外,Redis 是单线程的,不需要锁,但跳表的简单性对于 C 代码的可维护性有天然优势。
1 | // Redis 跳表节点 (redis/src/server.h) |