使用Go语言实现跳表(SkipList)

74 阅读4分钟

使用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
}

查找流程:

  1. 从最高层开始向右遍历
  2. 遇到较大值时向下层继续搜索
  3. 时间复杂度: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]
}

删除时需:

  1. 断开各层的指针连接
  2. 可能降低头节点高度

范围查询

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)

实现特点

  1. 动态层数调整:头节点层数随插入自动扩展,删除时自动收缩
  2. 概率平衡:通过随机层数避免严格的平衡操作
  3. 内存优化:节点仅存储必要的层指针
  4. 易扩展性:支持快速实现有序集合相关操作

使用示例

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
}