SkipList适用于需有序遍历、范围查询且支持高并发增删的场景;其核心是固定层数、原子指针更新与自底向上插入/删除,性能优于平衡树但慢于哈希表。

为什么不用标准库,而要自己写 SkipList
Go 标准库没有 SkipList,map 是哈希表实现,不支持按 key 有序遍历或范围查询(比如 “找所有大于 100 的键”);sort.Slice + 切片二分又无法高效插入删除。跳表在平均 O(log n) 时间内支持增删查+有序遍历,是平衡树的轻量替代方案。
常见误判是“用 container/list + 手动维护有序”,结果插入变成 O(n),很快卡住。跳表真正价值在于:插入/删除/查找/范围扫描全部保持对数时间,且实现比红黑树简单得多。
核心结构怎么定义才不踩内存和并发坑
跳表节点必须带多层指针,每层指向同层下一个节点。容易错在两处:一是用切片存指针([]*Node),导致每次插入都要扩容、内存不连续;二是没预分配层数,运行时随机生成层数但没限制上限,极端情况生成 20 层,浪费空间还拖慢遍历。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 固定最大层数(如 16),节点结构里直接定义
next [16]*Node,避免指针间接和切片开销 - 层数用
rand.Intn(16)不够好——应该用“抛硬币直到首次出现反面”的方式模拟几何分布:for rand.Int63()&1 == 0 { level++ },并设上限 - 如果要并发安全,别直接锁整个结构;用
sync.RWMutex锁写操作,读操作(如遍历、查找)可无锁,但需保证指针更新的原子性(靠 Go 的指针赋值天然原子)
Insert 和 Delete 怎么写才不漏节点或断链
跳表最常崩在更新多层指针时顺序错乱,比如先改高层 next 再改低层,中间被其他 goroutine 读到“半更新”状态,遍历就跳飞了。典型错误现象是:插入后查不到、范围查询漏数据、甚至 panic: invalid memory address。
关键逻辑:
- Insert 前先从顶层往下走,记录每一层“待插入位置前一个节点”(即 update 数组),再从底层往上逐层插入——确保低层已就位,高层才连上
- Delete 同样先走一遍拿到 update 数组,再从底层往上 unlink;不能边走边删,否则上层可能找不到前驱
- 所有指针赋值必须是单条语句,例如
update[i].next[i] = node.next[i],禁止拆成读-改-写三步(会破坏原子性)
示例片段(简化):
// 插入时更新第 i 层 node.next[i] = update[i].next[i] update[i].next[i] = node
和 map[int]int 比,什么时候真该用 SkipList
不是“想有序就换跳表”。它比哈希表慢约 3–5 倍(实测),内存多占 20%–40%。只在明确需要以下能力时值得引入:
- 频繁做范围查询:
Scan(from, to)或GreaterOrEqual(100) - 需要按 key 顺序迭代,且迭代过程中有插入/删除(
map迭代时修改会 panic) - 数据量大(>10⁵)、写多读少、且无法接受平衡树的复杂度或 GC 压力(比如嵌入式或实时服务)
最容易被忽略的是:跳表的“有序”是基于 key 的字节序或自定义比较函数,如果你的 key 是 string 但实际想按数字大小排,必须传 func(a, b string) bool { return atoi(a) ,否则 <code>"10" 成立——这个逻辑错位线上很难排查。


















