目录
正在加载目录…
专栏文章
专栏文章
概率型数据结构专栏
1. HyperLogLog:用 12KB 近似统计海量去重数 2. Count-Min Sketch:用 KB 内存估算海量数据频率 3. 跳表:用概率分层实现 O(log n) 有序索引 4. T-Digest:高精度估算 p99 与 p999 分位数

跳表:用概率分层实现 O(log n) 有序索引

发布于 2026-07-30 11:58 · 最后编辑于 2026-07-31 15:53 · 字数 1,151 👁 56 次阅读

跳表用概率随机化替代传统平衡树的严格旋转,实现了 O(log n) 期望复杂度的有序集合。Redis ZSet、LevelDB、RocksDB 均使用跳表作为核心数据结构。

目录

章节说明
核心思想为什么跳表比链表快
结构与原理多层链表与随机层高
核心操作查找、插入、删除的详细步骤
Java 实现完整可运行代码
复杂度分析期望时间与空间
对比与选型跳表 vs 红黑树 vs B+树

核心思想

问题:有序链表查找是 O(n),如何加速?

思路:建多级"快速通道"——

skiplist multilevel

查找 17:

  1. 第2层:1 → 9(跳过 1,3,4,6),9 < 17,继续
  2. 第2层:9 → 25(25 > 17),下降到第1层
  3. 第1层:9 → 17,找到!

概率随机化:每个节点插入时,以概率 p(通常 0.5)晋升到上一层。期望层高 = 1/(1-p) = 2(p=0.5 时),最高层数 log_{1/p}(n)。

结构与原理

skiplist node structure

随机层高:每层晋升概率 p = 0.5

int randomLevel() {
    int lvl = 1;
    while (random.nextDouble() < 0.5 && lvl < MAX_LEVEL) lvl++;
    return lvl;
}

期望分布:

  • 层高 1 的节点:50%
  • 层高 2 的节点:25%
  • 层高 3 的节点:12.5%
  • ...

核心操作

查找

从最高层开始,每层向右走直到 next > target,然后下降:

search(target):
  cur = head
  for i = maxLevel-1 downto 0:
    while cur.next[i] != null && cur.next[i].val < target:
      cur = cur.next[i]     // 在第 i 层向右移动
  cur = cur.next[0]         // 降到第0层后检查
  return cur != null && cur.val == target

插入

  1. 查找阶段记录每层的"前驱节点" update[]
  2. 随机生成新节点层高
  3. 更新各层指针
insert(val):
  update[0..MAX_LEVEL] = 记录每层需要更新 next 的前驱节点
  newLevel = randomLevel()
  newNode = Node(val, newLevel)
  for i = 0 to newLevel-1:
    newNode.next[i] = update[i].next[i]
    update[i].next[i] = newNode

删除

同样需要先找 update[],然后摘除各层指针:

delete(val):
  update[] = 同上
  target = update[0].next[0]
  if target == null || target.val != val: return false
  for i = 0 to level-1:
    if update[i].next[i] != target: break
    update[i].next[i] = target.next[i]
  return true

Java 实现

import java.util.Random;

/**
 * 跳表:支持有序集合的 add / delete / search,期望 O(log n)
 */
public class SkipList {
    private static final int MAX_LEVEL = 16;
    private static final double P = 0.5;

    private final Node head;
    private int level; // 当前最大有效层数
    private final Random random;

    static class Node {
        int val;
        Node[] next;
        Node(int val, int level) {
            this.val = val;
            this.next = new Node[level];
        }
    }

    public SkipList() {
        head = new Node(Integer.MIN_VALUE, MAX_LEVEL);
        level = 1;
        random = new Random(42);
    }

    private int randomLevel() {
        int lvl = 1;
        while (random.nextDouble() < P && lvl < MAX_LEVEL) lvl++;
        return lvl;
    }

    public boolean search(int target) {
        Node cur = head;
        for (int i = level - 1; i >= 0; i--) {
            while (cur.next[i] != null && cur.next[i].val < target)
                cur = cur.next[i];
        }
        cur = cur.next[0];
        return cur != null && cur.val == target;
    }

    public void insert(int val) {
        Node[] update = new Node[MAX_LEVEL];
        Node cur = head;
        for (int i = level - 1; i >= 0; i--) {
            while (cur.next[i] != null && cur.next[i].val < val)
                cur = cur.next[i];
            update[i] = cur;
        }
        // 已存在则不重复插入
        if (cur.next[0] != null && cur.next[0].val == val) return;

        int newLevel = randomLevel();
        if (newLevel > level) {
            for (int i = level; i < newLevel; i++) update[i] = head;
            level = newLevel;
        }
        Node newNode = new Node(val, newLevel);
        for (int i = 0; i < newLevel; i++) {
            newNode.next[i] = update[i].next[i];
            update[i].next[i] = newNode;
        }
    }

    public boolean delete(int val) {
        Node[] update = new Node[MAX_LEVEL];
        Node cur = head;
        for (int i = level - 1; i >= 0; i--) {
            while (cur.next[i] != null && cur.next[i].val < val)
                cur = cur.next[i];
            update[i] = cur;
        }
        Node target = cur.next[0];
        if (target == null || target.val != val) return false;

        for (int i = 0; i < level; i++) {
            if (update[i].next[i] != target) break;
            update[i].next[i] = target.next[i];
        }
        while (level > 1 && head.next[level - 1] == null) level--;
        return true;
    }
}

验证结果(插入 10 个元素,查找、删除、重复插入、1000 元素有序插入):全部测试通过 ✓

复杂度分析

操作期望时间最坏时间(极端随机)
查找O(log n)O(n)
插入O(log n)O(n)
删除O(log n)O(n)
空间O(n)O(n log n)

为什么是期望 O(log n):层高由随机化控制,期望第 k 层节点数为 n/2^k,查找时每层期望移动 1/p = 2 步,总层数约 log_{1/p}(n),故总步数期望为 2 * log₂n。

与确定性平衡树的区别:跳表无旋转操作,实现简单;代价是最坏情况概率性地变差(实践中极少发生)。

对比与选型

维度跳表红黑树B+树
实现复杂度高(旋转逻辑复杂)
时间复杂度O(log n) 期望O(log n) 确定O(log n) 确定
范围查询优(第0层天然有序)中(中序遍历)优(叶节点链表)
内存局部性差(指针跳转多)优(磁盘友好)
并发支持优(锁粒度细)差(旋转需全局锁)
适用场景内存有序集合、RedisJVM 内(TreeMap)磁盘存储(数据库)

Redis 选择跳表而非红黑树的原因:

  1. 实现更简单,易于调试
  2. 范围查询(ZRANGEBYSCORE)效率相同
  3. 并发场景下锁粒度更细

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论