
跳表在 C++ 中不是标准库组件,得自己写或选对第三方实现
标准 C++ 没有 skiplist,STL 的 std::set 和 std::map 是红黑树,平均 O(log n) 但常数小、缓存友好;跳表虽理论也是 O(log n),但随机指针跳跃导致 CPU 缓存不友好,实际吞吐未必更高——除非你明确需要高并发写入(跳表的插入可无锁)、或正在仿写 LevelDB / Redis Sorted Set 底层。
常见错误是直接照搬教科书伪代码写单线程跳表,结果发现比 std::map 慢 2–3 倍,原因在于指针跳转打乱内存局部性。真实数据库索引里用跳表,几乎都配套做了节点内存池 + 定长 slab 分配 + level 高度预裁剪。
- 别从零手撸带随机数的
randomLevel():生产环境用 PCG 或 XorShift 替代rand(),避免种子冲突和周期短 - level 0 节点必须存完整 key-value,高层只存 key + 指针;否则范围查询时没法回溯
- Redis 的
zset跳表每个节点还挂一个dicthash 表指针,用于 O(1) 查 rank —— 单纯跳表不支持按序号查元素
并发安全不能靠 std::mutex 粗粒度锁整个跳表
加一把全局锁会让跳表退化成链表级性能,尤其写多场景下,所有插入/删除都排队。LevelDB 和 RocksDB 的跳表实现默认禁用并发写,靠上层 WAL + memtable 切换规避;真要支持并发,得按 level 分段加锁,或用无锁技巧。
典型坑是认为“跳表天然适合无锁”,其实不然:compare_exchange_weak 在多层指针更新时极易 ABA 问题,尤其 level 数动态变化时。Facebook 的 Folly 库提供 folly::Synchronized 包装的跳表,但内部仍是 per-level mutex,不是完全无锁。
立即学习“C++免费学习笔记(深入)”;
- 写操作只锁涉及的 level 范围,比如插入高度为 3 的节点,只需锁 level 0–3 对应的前驱节点
- 读操作可完全无锁,但需保证指针读取的原子性(
std::atomic<t></t>,不是裸指针) - 删节点时必须标记再回收(hazard pointer 或 epoch-based reclamation),否则其他线程正遍历该节点会 crash
std::set 够用就别硬上跳表,除非你要支持范围截断 + 并发写 + 内存可控
跳表唯一不可替代的场景是:既要 O(log n) 插入/查询,又要 O(1) 获取某段 rank 范围(如 “第 1000–1010 名”),且写入压力大到红黑树锁竞争明显。MySQL 的自增主键索引、PostgreSQL 的 B-tree 都不换跳表,因为 B-tree 更稳、更省内存、范围扫描更快。
如果你只是想做个本地缓存的有序结构,std::set + std::lower_bound 就行;想做分布式协同排序,那应该看 CRDT 或逻辑时钟,不是跳表。
- 跳表内存占用通常是红黑树的 2–4 倍(每节点多个指针 + 随机 level)
- 高度概率分布是关键参数:
p = 0.5时平均高度 log₂n,但实际常设p = 0.25控制内存 - LevelDB 中跳表最大 level 写死为 12,避免极端情况爆炸,你也得设上限
调试跳表最常卡在指针误连和内存释放时机
跳表 debug 的噩梦不是逻辑错,而是指针连错一层、漏删中间 level、或提前 free 了还在被其他线程读的节点。AddressSanitizer 能抓野指针,但抓不到逻辑错位——比如 update[i] 数组没正确记录每层前驱,导致插入后链断裂。
建议在构造函数里开个 validate() 方法,每次增删后跑一遍:检查每层指针是否单调递增、各层节点数是否符合概率分布、level 0 是否包含全部元素。别等线上查不到数据才怀疑跳表。
- 用
std::shared_ptr管理节点?不行,循环引用风险高;改用std::unique_ptr+ raw pointer 组合,所有权清晰 - 打印调试时别只输出 key,要带 level 和各层 next 指针地址,比如:
Node(42, level=3) -> [0xabc, 0xdef, nullptr, 0x123] - 单元测试必须覆盖高度为 1、最大值、重复 key、跨 level 删除等边界,否则上线后静默丢数据
next[level] 赋值里。











