用Golang实现跳表:动态实现详解

60 阅读6分钟

1.前言

在处理有序数据时,我们经常会遇到这样的问题:

  • 查找慢:链表虽然灵活,但查找操作需要从头遍历,时间复杂度为 O(n)
  • 插入删除代价高:数组支持随机访问,但在中间插入或删除元素时,需要移动大量元素,效率不高
  • 动态扩展困难:静态数组在容量固定的情况下,难以应对数据量增长或频繁变动的场景 为了在有序数据管理中同时兼顾查找效率和插入删除灵活性,计算机科学家提出了多种解决方案,其中平衡二叉搜索树(如 AVL 树、红黑树)是一种经典选择。然而,平衡树在实现上较为复杂,需要在插入和删除时进行旋转操作,代码逻辑相对繁琐 而跳表则提供了一种更简单、灵活且高效的替代方案。它的核心思想是在普通链表的基础上增加多层“索引”,通过多级跳跃来加快查找速度。跳表在平均情况下能够实现O(log n)的查找、插入和删除操作,同时保持动态可扩展性。这种概率平衡的方式,比严格平衡树更易于实现,而且性能稳定

跳表不仅是一个理论上的数据结构,在实际系统中也有广泛应用:

  • Redis ZSet:底层使用跳表 + 哈希表来实现有序集合,保证分数排序和快速查找
  • 排行榜系统:游戏积分榜、社交排行榜等场景需要快速查询和更新排名
  • 延迟队列和任务调度:跳表可以按时间戳排序,便于高效执行到期任务
  • 时间序列数据处理:日志、监控数据按时间排序查询

在本篇文章中中,我会从基础概念、原理分析,到静态和动态两种实现方式全面讲解跳表

  • 详细讲解跳表的核心原理:多层链表、查找、插入、删除
  • 提供动态链表版本的实现和代码讲解

2.跳表的概念和原理

2.1 跳表的定义

跳表(Skip List)是一种动态数据结构,它在有序链表的基础上增加多级索引,使得查找、插入和删除操作的平均时间复杂度可以达到 O(log⁡n)O(\log n)O(logn) 跳表的核心思想是:

  • 保持底层是一个有序链表,保证数据的顺序性。
  • 在底层之上随机或按规则建立多层索引,每一层跳过若干节点,用于加速查找 相比于平衡二叉搜索树(AVL、红黑树),跳表实现更简单,同时支持动态增删改查

2.2 跳表的结构

假设我们现在有一个有序链表 Level1底层链表 在这里插入图片描述 跳表会在上层建立索引层,跳过一些节点,例如Level2: 在这里插入图片描述 再往上,如果有Level3: 在这里插入图片描述 结合多层表示: 在这里插入图片描述

  • Level 1 是底层链表,存储所有节点
  • 每向上一层,节点数量会减少,查找时可以快速跳过中间节点
  • 随机化插入保证了每层节点的分布均匀,平均查找效率 O(log⁡n)O(\log n)O(logn)

2.3 跳表的原理

跳表操作基于逐层搜索 + 层级下降:

  1. 查找(Search)
    • 从最高层的头节点开始向右遍历,直到遇到比目标大的节点或到达当前层尾部
    • 向下到下一层继续向右搜索,直到到达底层
    • 如果在底层找到 key,则返回节点,否则说明不存在
  2. 插入(Add)
    • 先查找目标位置的前驱节点
    • 随机生成新节点的层数

为什么要用随机化

  1. 防止分布不均匀

  • 如果每 N 个节点固定生成一层,可能出现节点集中在某些区域,高层索引不均衡。

  • 查询时会遇到“高层空洞”或“密集区”,查找效率下降

  1. 实现简单

  • 比 AVL、红黑树等平衡树更容易实现,不需要旋转或复杂平衡操作

  1. 保证平均复杂度

  • 随机分布保证每层节点数量大约减半,从而平均查找、插入、删除时间复杂度都是 O(log⁡n)O(\log n)O(logn)
  • 从底层到新节点的最高层,将新节点插入到每一层对应位置,更新前驱节点的 next 指针

原链表: 在这里插入图片描述 插入一个值为55的节点,随机的层数为2: 在这里插入图片描述

  1. 删除(Erase)
  • 类似查找,找到要删除节点的前驱节点
  • 从底层到节点的最高层,将前驱节点的 next 指针指向被删除节点的下一节点
  • 如果最高层为空,则跳表层数减少 在这里插入图片描述

3.具体代码实现与讲解

3.1 动态结构实现

下面将会展示一个简单跳表的动态实现(根据leetcode中给出的结构进行示例)

package main

import (
    "math/rand"
    "time"
)

// 最大层数和随机晋升概率
const (
    MaxLevel    = 20  // 跳表允许的最大层数
    Probability = 0.5 // 每次向上一层晋升的概率
)

// 节点结构体
type Node struct {
    key  int     // 节点值
    next []*Node // 每一层的指针
}

// 跳表结构体
type Skiplist struct {
    head  *Node // 哨兵头节点
    level int   // 当前跳表最高层
}

// 构造函数
func Constructor() Skiplist {
    rand.Seed(time.Now().UnixNano()) // 初始化随机种子
    return Skiplist{
       head: &Node{
          next: make([]*Node, MaxLevel), // 头节点每层指针初始化为 nil
       },
       level: 1, // 初始只有底层
    }
}

// 随机生成节点层数
func (s *Skiplist) randomLevel() int {
    level := 1
    // 每次向上一层的概率为 Probability
    for rand.Float64() < Probability && level < MaxLevel {
       level++
    }
    // 返回节点的层数,保证底层至少为1
    return level
}

// 内部查找,返回节点指针
func (s *Skiplist) search(key int) *Node {
    cur := s.head
    // 从最高层开始逐层向下
    for i := s.level - 1; i >= 0; i-- {
       // 在当前层水平移动,直到找到下一个节点大于等于 key
       for cur.next[i] != nil && cur.next[i].key < key {
          cur = cur.next[i]
       }
       // 如果找到了 key,直接返回节点
       if cur.next[i] != nil && cur.next[i].key == key {
          return cur.next[i]
       }
    }
    // 没找到返回 nil
    return nil
}

// 对外查找接口
func (s *Skiplist) Search(key int) bool {
    // 调用内部 search 函数
    return s.search(key) != nil
}

// 插入节点
func (s *Skiplist) Add(key int) {
    level := s.randomLevel() // 随机生成节点层数
    if level > s.level {
       s.level = level // 更新跳表最高层
    }

    // 创建新节点,切片长度为节点层数
    newNode := &Node{
       key:  key,
       next: make([]*Node, level),
    }

    cur := s.head
    // 从新节点最高层向下插入
    for i := level - 1; i >= 0; i-- {
       // 在当前层找到前驱节点
       for cur.next[i] != nil && cur.next[i].key < key {
          cur = cur.next[i]
       }
       // 指针调整:新节点指向前驱节点的下一个节点
       newNode.next[i] = cur.next[i]
       // 前驱节点指向新节点
       cur.next[i] = newNode
    }
}

// 删除节点
func (s *Skiplist) Erase(key int) bool {
    if s.search(key) == nil {
       return false // 节点不存在
    }

    cur := s.head
    // 从最高层向下删除
    for i := s.level - 1; i >= 0; i-- {
       // 找到当前层前驱节点
       for cur.next[i] != nil && cur.next[i].key < key {
          cur = cur.next[i]
       }
       // 如果下一个节点是目标节点,删除它
       if cur.next[i] != nil && cur.next[i].key == key {
          cur.next[i] = cur.next[i].next[i]
       }
    }
    // 调整跳表层数:去掉空的最高层
    for s.level > 1 && s.head.next[s.level-1] == nil {
       s.level--
    }
    return true
}

后面会写关于链表的静态数据结构的代码,会展示更多的功能