使用Go语言实现跳表(Skip List)
什么是跳表?
跳表(Skip List)是一种概率平衡的数据结构,通过多层有序链表实现高效查找。其平均时间复杂度为O(log n),与平衡树相当,但实现更为简单。跳表广泛应用于Redis等系统中。
结构定义
节点结构
type node struct {
nexts []*node // 各层后继指针
key int // 键
val int // 值
}
每个节点包含:
nexts
:多级指针数组,nexts[i]
表示第i层的下一个节点key
/val
:存储的键值对
跳表主体
type SkipList struct {
head *node // 头节点(不存储实际数据)
}
头节点的nexts
数组长度决定当前跳表的最大层数。
核心操作实现
查找操作
func (list *SkipList) Get(key int) (int, bool) {
if node := list.search(key); node != nil {
return node.val, true
}
return -1, false
}
func (list *SkipList) search(key int) *node {
move := list.head
for level := len(list.head.nexts)-1; level >= 0; level-- {
for move.nexts[level] != nil && move.nexts[level].key < key {
move = move.nexts[level]
}
if move.nexts[level] != nil && move.nexts[level].key == key {
return move.nexts[level]
}
}
return nil
}
查找流程:
- 从最高层开始向右遍历
- 遇到较大值时向下层继续搜索
- 时间复杂度:O(log n)
插入操作
func (list *SkipList) Put(key, val int) {
if node := list.search(key); node != nil { // 已存在则更新
node.val = val
return
}
level := list.roll() // 随机层数
for len(list.head.nexts)-1 < level { // 扩展头节点层数
list.head.nexts = append(list.head.nexts, nil)
}
newNode := node{
key: key,
val: val,
nexts: make([]*node, level+1),
}
move := list.head
for lv := level; lv >= 0; lv-- { // 逐层插入
for move.nexts[lv] != nil && move.nexts[lv].key < key {
move = move.nexts[lv]
}
newNode.nexts[lv] = move.nexts[lv]
move.nexts[lv] = &newNode
}
}
func (list *SkipList) roll() int {
level := 0
for rand.Intn(2) == 0 { // 50%概率增加层数
level++
}
return level
}
关键点:
- 随机层数生成:遵循"抛硬币"规则
- 动态调整头节点高度
- 插入时间复杂度:O(log n)
删除操作
func (list *SkipList) Del(key int) {
// ...(搜索与删除逻辑)
// 缩容处理
dif := 0
for level := len(list.head.nexts)-1;
level >= 0 && list.head.nexts[level] == nil;
level-- {
dif++
}
list.head.nexts = list.head.nexts[:len(list.head.nexts)-dif]
}
删除时需:
- 断开各层的指针连接
- 可能降低头节点高度
范围查询
func (list *SkipList) Range(start, end int) [][2]int {
ceilNode := list.ceiling(start)
// 遍历底层链表收集结果...
}
利用底层链表的有序性,实现O(k)时间复杂度的范围查询(k为结果数量)
特性方法
Ceiling/Floor
Ceiling(key)
:返回不小于key的最小键Floor(key)
:返回不大于key的最大键
// 示例:Floor实现
func (list *SkipList) floor(target int) *node {
move := list.head
for level := len(list.head.nexts)-1; level >= 0; level-- {
for move.nexts[level] != nil && move.nexts[level].key < target {
move = move.nexts[level]
}
}
return move // 最终停留在<=target的最后一个节点
}
性能分析
操作 | 平均时间复杂度 | 最坏情况 |
---|---|---|
查找 | O(log n) | O(n) |
插入 | O(log n) | O(n) |
删除 | O(log n) | O(n) |
范围查询 | O(k) | O(n) |
实现特点
- 动态层数调整:头节点层数随插入自动扩展,删除时自动收缩
- 概率平衡:通过随机层数避免严格的平衡操作
- 内存优化:节点仅存储必要的层指针
- 易扩展性:支持快速实现有序集合相关操作
使用示例
func main() {
sl := &SkipList{head: &node{}}
sl.Put(3, 30)
sl.Put(1, 10)
sl.Put(2, 20)
fmt.Println(sl.Get(2)) // 20, true
fmt.Println(sl.Range(1, 3)) // [[1 10] [2 20] [3 30]]
}
并发安全的跳表
读写锁 sync.RWMutex 版
使用sync.RWMutex
实现读写分离锁, 读操作(Get/Range等)使用RLock()
, 写操作(Put/Del)使用Lock()
package main
import (
"math/rand"
"sync"
)
type SkipList struct {
head *node
mu sync.RWMutex // 读写锁
}
type node struct {
nexts []*node
key, val int
}
// Get 线程安全查询
func (list *SkipList) Get(key int) (int, bool) {
list.mu.RLock() // 读锁
defer list.mu.RUnlock() // 确保解锁
if node := list.search(key); node != nil {
return node.val, true
}
return -1, false
}
// Put 线程安全插入
func (list *SkipList) Put(key, val int) {
list.mu.Lock() // 写锁
defer list.mu.Unlock() // 确保解锁
// 原有插入逻辑保持不变...
}
// Del 线程安全删除
func (list *SkipList) Del(key int) {
list.mu.Lock()
defer list.mu.Unlock()
// 原有删除逻辑保持不变...
}
// Range 线程安全范围查询
func (list *SkipList) Range(start, end int) [][2]int {
list.mu.RLock()
defer list.mu.RUnlock()
// 原有范围查询逻辑保持不变...
}
// Ceiling/Floor 方法也需要加读锁
func (list *SkipList) Ceiling(target int) ([2]int, bool) {
list.mu.RLock()
defer list.mu.RUnlock()
// ...
}
func (list *SkipList) Floor(target int) ([2]int, bool) {
list.mu.RLock()
defer list.mu.RUnlock()
// ...
}
// 以下内部方法不需要加锁(由外部方法统一控制)
func (list *SkipList) search(key int) *node {
// 原有实现保持不变...
}
func (list *SkipList) roll() int {
// 原有实现保持不变...
}
// 其他辅助方法保持不变...
细粒度锁(适用于超高并发场景)
使用读写锁可以在读多写少的场景下提高并发能力,但写操作会阻塞所有读写操作。如果写操作频繁,可能会影响性能,这时候可能需要考虑其他并发控制机制,比如分段锁,但实现起来更复杂。不过对于一般情况,读写锁已经足够。
const segmentCount = 32
type ConcurrentSkipList struct {
segments [segmentCount]*SkipList
locks [segmentCount]sync.RWMutex
}
func (c *ConcurrentSkipList) getSegment(key int) int {
return key % segmentCount
}