跳表(Skip List)详解:原理、实现、使用场景与实际应用

跳表(Skip List)是一种概率型数据结构,由 William Pugh 于 1989 年提出。它通过"多级索引"在有序链表之上建立快速查找路径,从而在期望意义上达到与平衡树相近的 O(log n) 查找、插入和删除复杂度。跳表的最大特点是:实现简单、支持范围查询、天然适合并发场景

本文将从原理、复杂度、使用场景、实际应用,以及 Java / Python 代码示例几个方面完整介绍跳表。


一、跳表的核心思想

普通有序链表查找一个元素需要从头遍历,时间复杂度是 O(n)

例如:

text 复制代码
head -> 3 -> 6 -> 7 -> 9 -> 12 -> 17 -> 19 -> 21 -> 25 -> 26 -> null

如果要找 25,必须从头一个个比较。

跳表的做法是:在原始链表之上建立多级索引。高层索引跨度大,用于快速跳跃;底层链表保存全部元素,用于精确查找。

例如:

text 复制代码
Level 2: head -------------------------> 21 -> null
Level 1: head -------> 7 -> 12 -------> 21 -> null
Level 0: head -> 3 -> 6 -> 7 -> 9 -> 12 -> 17 -> 19 -> 21 -> 25 -> 26 -> null

查找 25 时:

  1. 从最高层 Level 2 开始,向右走到 21
  2. 发现下一个是 null,下降到 Level 1
  3. 21 后面没有更合适的节点,下降到 Level 0
  4. 21 继续向右,找到 25

这样跳过了大量节点。


二、跳表的结构

跳表由节点组成,每个节点包含:

  • value:存储的值;
  • forward[]:指向不同层级的下一个节点的指针数组。

例如一个节点的 forward 长度为 4,表示它出现在第 0、1、2、3 层。

text 复制代码
Node:
  value = 12
  forward[0] -> 17
  forward[1] -> 19
  forward[2] -> null

跳表还有一个头节点 head,它通常拥有最大层数,但不存储实际数据。


三、跳表的核心操作

1. 查找

从最高层开始:

  • 如果当前节点的下一个节点值小于目标值,就向右移动;
  • 否则下降到下一层;
  • 到达第 0 层后,检查下一个节点是否等于目标值。

伪代码:

text 复制代码
cur = head
for level from maxLevel down to 0:
    while cur.forward[level] != null and cur.forward[level].value < target:
        cur = cur.forward[level]
cur = cur.forward[0]
return cur != null and cur.value == target

2. 插入

插入时先查找每一层的前驱节点,记录到 update[] 数组中。然后随机生成新节点的层数,最后逐层插入。

关键点:新节点的层数由随机函数决定

例如:

text 复制代码
while random() < p and level < MAX_LEVEL:
    level++

通常 p = 0.5p = 0.25p 越小,高层节点越少,空间越省,但查找稍慢。

3. 删除

删除时同样先找到每一层的前驱节点,然后逐层断开指针。

如果删除后最高层为空,就降低跳表当前最大层数。


四、复杂度分析

假设晋升概率为 p,节点总数为 n

操作 期望复杂度 最坏复杂度
查找 O(log n) O(n)
插入 O(log n) O(n)
删除 O(log n) O(n)
空间 O(n) O(n log n)

说明:

  • 期望层数约为 log_{1/p} n
  • 每一层期望访问 1/p 个节点;
  • 因此查找总步数期望为 O(log n)
  • 每个节点的期望层数为 1 / (1 - p),所以总指针数期望为 O(n)
  • 最坏情况理论上可能退化成普通链表,但概率极低。

例如 p = 0.5,100 万个元素期望层数约为 log2(1,000,000) ≈ 20。所以 MAX_LEVEL = 32 通常足够。


五、跳表 vs 平衡树 vs 哈希表

特性 跳表 平衡树 哈希表
平均查找 O(log n) O(log n) O(1)
最坏查找 O(n),概率极低 O(log n) O(n)
是否有序
范围查询 非常自然 支持 不支持
实现难度 简单 较复杂 简单
并发实现 较容易 较难 扩容较难
内存开销 多级指针 平衡因子/颜色 桶 + 链表/树

跳表的优势:

  • 不需要旋转,不需要复杂平衡逻辑;
  • 范围查询、排序遍历非常方便;
  • 并发插入删除更容易实现;
  • 代码量通常远少于红黑树、AVL 树。

跳表的劣势:

  • 查找是期望 O(log n),不是严格最坏 O(log n)
  • 指针较多,缓存局部性可能不如 B+ 树;
  • 磁盘存储场景通常还是 B+ 树更合适。

六、使用场景

跳表适合以下场景:

  1. 有序集合 / 有序映射

    • 需要按 key 排序;
    • 需要范围查询,如 [start, end]
    • 需要排名、Top N、前后驱查询。
  2. 排行榜

    • 按分数排序;
    • 查询某个用户排名;
    • 查询前 100 名;
    • 查询分数区间内的用户。
  3. 延时队列

    • 按到期时间排序;
    • 快速取出最早到期的任务。
  4. 内存数据库索引

    • 数据在内存中,需要有序结构;
    • 写入和范围扫描都很频繁。
  5. 并发有序结构

    • 多线程同时读写;
    • 不希望使用全局锁;
    • Java 的 ConcurrentSkipListMap 就是典型代表。
  6. LSM-Tree 的 MemTable

    • 写入先进入内存跳表;
    • 跳表保持有序;
    • 后续 flush 成 SSTable。

七、实际应用

1. Redis 有序集合 ZSET

Redis 的 ZSET 是最著名的跳表应用之一。

ZSET 同时使用:

  • 哈希表:member -> score,实现 O(1) 查分数;
  • 跳表:按 score 排序,支持范围查询和排名。

Redis 跳表特点:

  • 最大层数 ZSKIPLIST_MAXLEVEL = 32
  • 晋升概率 ZSKIPLIST_P = 0.25
  • 每个节点有 backward 指针,支持逆序遍历;
  • 每个层级指针有 span,用于计算排名 ZRANK

常见命令:

text 复制代码
ZADD ranking 100 user1
ZADD ranking 200 user2
ZRANGE ranking 0 -1 WITHSCORES
ZRANK ranking user1
ZRANGEBYSCORE ranking 100 200

这些操作背后都依赖跳表。

2. Java ConcurrentSkipListMap / ConcurrentSkipListSet

Java 标准库提供了:

  • ConcurrentSkipListMap
  • ConcurrentSkipListSet

它们是线程安全的有序 Map / Set,基于跳表实现,支持:

  • 高并发读写;
  • 范围查询;
  • 有序遍历;
  • 弱一致性迭代器。

HBase 的 MemStore 就使用了 ConcurrentSkipListMap 来维护内存中的有序数据。

3. LevelDB / RocksDB MemTable

LevelDB 和 RocksDB 的 MemTable 使用跳表作为内存表:

  • 写入先写 MemTable;
  • MemTable 按 key 有序;
  • 读操作可以在内存中快速查找;
  • 写满后冻结并 flush 到 SSTable。

为什么用跳表而不是平衡树?

  • 跳表实现简单;
  • 并发写入更容易;
  • 不需要旋转;
  • 范围扫描自然。

4. Lucene 倒排索引

Lucene 的倒排索引中,postings list 会使用 skip list 结构加速文档号跳跃。例如多个词项求交集时,可以通过跳表指针快速跳过不可能匹配的文档。

5. 其他应用

  • 排行榜系统;
  • 实时风控的有序窗口;
  • 时间线排序;
  • 分布式系统中的有序索引;
  • 内存缓存的有序淘汰策略。

八、Java 实现示例

下面是一个简化版、线程不安全的跳表实现,用于教学。

java 复制代码
import java.util.Random;

public class SkipList<T extends Comparable<T>> {
    private static final int MAX_LEVEL = 16;
    private static final double P = 0.5;

    private static class Node<T> {
        T value;
        Node<T>[] forward;

        @SuppressWarnings("unchecked")
        Node(T value, int level) {
            this.value = value;
            this.forward = (Node<T>[]) new Node[level + 1];
        }
    }

    private final Node<T> head;
    private int level;
    private final Random random;

    @SuppressWarnings("unchecked")
    public SkipList() {
        this.head = new Node<>(null, MAX_LEVEL);
        this.level = 0;
        this.random = new Random();
    }

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

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

    @SuppressWarnings("unchecked")
    public void insert(T value) {
        Node<T>[] update = (Node<T>[]) new Node[MAX_LEVEL + 1];
        Node<T> cur = head;

        for (int i = level; i >= 0; i--) {
            while (cur.forward[i] != null &&
                    cur.forward[i].value.compareTo(value) < 0) {
                cur = cur.forward[i];
            }
            update[i] = cur;
        }

        cur = cur.forward[0];
        if (cur != null && cur.value.compareTo(value) == 0) {
            return; // 简化:不插入重复值
        }

        int newLevel = randomLevel();

        if (newLevel > level) {
            for (int i = level + 1; i <= newLevel; i++) {
                update[i] = head;
            }
            level = newLevel;
        }

        Node<T> newNode = new Node<>(value, newLevel);

        for (int i = 0; i <= newLevel; i++) {
            newNode.forward[i] = update[i].forward[i];
            update[i].forward[i] = newNode;
        }
    }

    @SuppressWarnings("unchecked")
    public boolean delete(T value) {
        Node<T>[] update = (Node<T>[]) new Node[MAX_LEVEL + 1];
        Node<T> cur = head;

        for (int i = level; i >= 0; i--) {
            while (cur.forward[i] != null &&
                    cur.forward[i].value.compareTo(value) < 0) {
                cur = cur.forward[i];
            }
            update[i] = cur;
        }

        cur = cur.forward[0];

        if (cur == null || cur.value.compareTo(value) != 0) {
            return false;
        }

        for (int i = 0; i <= level; i++) {
            if (update[i].forward[i] != cur) {
                break;
            }
            update[i].forward[i] = cur.forward[i];
        }

        while (level > 0 && head.forward[level] == null) {
            level--;
        }

        return true;
    }

    public void print() {
        for (int i = level; i >= 0; i--) {
            Node<T> cur = head.forward[i];
            System.out.print("Level " + i + ": ");
            while (cur != null) {
                System.out.print(cur.value + " -> ");
                cur = cur.forward[i];
            }
            System.out.println("null");
        }
    }

    public static void main(String[] args) {
        SkipList<Integer> skipList = new SkipList<>();
        int[] nums = {3, 6, 7, 9, 12, 19, 17, 26, 21, 25};

        for (int num : nums) {
            skipList.insert(num);
        }

        skipList.print();

        System.out.println("search 19: " + skipList.search(19));
        System.out.println("search 15: " + skipList.search(15));

        skipList.delete(19);
        System.out.println("after delete 19, search 19: " + skipList.search(19));
    }
}

Java 标准库示例

生产环境中,更推荐直接使用 ConcurrentSkipListMap

java 复制代码
import java.util.concurrent.ConcurrentSkipListMap;

public class ConcurrentSkipListDemo {
    public static void main(String[] args) {
        ConcurrentSkipListMap<Integer, String> map = new ConcurrentSkipListMap<>();

        map.put(3, "C");
        map.put(1, "A");
        map.put(5, "E");
        map.put(2, "B");
        map.put(4, "D");

        System.out.println(map.firstKey()); // 1
        System.out.println(map.lastKey());  // 5
        System.out.println(map.ceilingKey(3)); // 3
        System.out.println(map.floorKey(3)); // 3
        System.out.println(map.subMap(2, true, 4, true));
        // {2=B, 3=C, 4=D}
    }
}

九、Python 实现示例

Python 标准库没有内置跳表,但可以手写一个简化版本。

python 复制代码
import random


class Node:
    __slots__ = ("value", "forward")

    def __init__(self, value=None, level=0):
        self.value = value
        self.forward = [None] * (level + 1)


class SkipList:
    def __init__(self, max_level=16, p=0.5):
        self.max_level = max_level
        self.p = p
        self.level = 0
        self.head = Node(None, max_level)
        self._random = random.Random()

    def _random_level(self):
        lvl = 0
        while lvl < self.max_level and self._random.random() < self.p:
            lvl += 1
        return lvl

    def search(self, target):
        cur = self.head

        for i in range(self.level, -1, -1):
            while cur.forward[i] is not None and cur.forward[i].value < target:
                cur = cur.forward[i]

        cur = cur.forward[0]
        return cur is not None and cur.value == target

    def insert(self, value):
        update = [None] * (self.max_level + 1)
        cur = self.head

        for i in range(self.level, -1, -1):
            while cur.forward[i] is not None and cur.forward[i].value < value:
                cur = cur.forward[i]
            update[i] = cur

        cur = cur.forward[0]

        if cur is not None and cur.value == value:
            return  # 简化:不插入重复值

        new_level = self._random_level()

        if new_level > self.level:
            for i in range(self.level + 1, new_level + 1):
                update[i] = self.head
            self.level = new_level

        new_node = Node(value, new_level)

        for i in range(new_level + 1):
            new_node.forward[i] = update[i].forward[i]
            update[i].forward[i] = new_node

    def delete(self, value):
        update = [None] * (self.max_level + 1)
        cur = self.head

        for i in range(self.level, -1, -1):
            while cur.forward[i] is not None and cur.forward[i].value < value:
                cur = cur.forward[i]
            update[i] = cur

        cur = cur.forward[0]

        if cur is None or cur.value != value:
            return False

        for i in range(self.level + 1):
            if update[i].forward[i] is not cur:
                break
            update[i].forward[i] = cur.forward[i]

        while self.level > 0 and self.head.forward[self.level] is None:
            self.level -= 1

        return True

    def print_all(self):
        for i in range(self.level, -1, -1):
            cur = self.head.forward[i]
            values = []
            while cur is not None:
                values.append(str(cur.value))
                cur = cur.forward[i]
            print(f"Level {i}: " + " -> ".join(values))


if __name__ == "__main__":
    sl = SkipList()

    for x in [3, 6, 7, 9, 12, 19, 17, 26, 21, 25]:
        sl.insert(x)

    sl.print_all()

    print("search 19:", sl.search(19))
    print("search 15:", sl.search(15))

    sl.delete(19)
    print("after delete 19, search 19:", sl.search(19))

十、工程注意事项

  1. 随机层数使用线程安全随机源

    • 并发环境下不要使用共享的普通 Random
    • Java 可用 ThreadLocalRandom
    • 或者直接使用 ConcurrentSkipListMap
  2. 选择合适的 pMAX_LEVEL

    • p = 0.5:实现简单,空间约 2n 指针;
    • p = 0.25:空间更省,Redis 采用;
    • MAX_LEVEL 一般取 32 或 64;
    • 元素规模越大,层数需求越高。
  3. 重复值处理

    • 本文示例不允许重复;
    • 如果需要重复,可以增加 count 字段;
    • 或使用复合 key,如 (value, id)
  4. 范围查询

    • 先找到 >= start 的第一个节点;
    • 然后沿第 0 层向后遍历到 > end 停止;
    • 这是跳表非常强的能力。
  5. 排名查询

    • 普通跳表不直接支持 rank
    • Redis 在每个指针中维护 span,从而支持 ZRANK
  6. 生产环境优先使用成熟实现

    • Java:ConcurrentSkipListMapConcurrentSkipListSet
    • Redis:ZSET
    • LevelDB / RocksDB:MemTable;
    • Python:可使用 Redis,或第三方有序容器库。

十一、总结

跳表是一种用"概率平衡"代替"严格平衡"的数据结构。它通过在有序链表上建立多级索引,实现了期望 O(log n) 的查找、插入和删除。

它的核心优势是:

  • 实现简单;
  • 支持有序遍历和范围查询;
  • 并发实现相对容易;
  • 在内存索引、排行榜、延时队列、LSM-Tree MemTable 等场景中非常实用。

实际工程中,Redis 的 ZSET、Java 的 ConcurrentSkipListMap、LevelDB / RocksDB 的 MemTable 都是跳表的经典应用。理解跳表,不仅能掌握一种高效数据结构,也能更好理解现代存储系统和并发容器的设计思路。

相关推荐
miller-tsunami1 小时前
排序的相关知识点
数据结构·排序算法
Logic1015 小时前
C语言/数据结构滑动窗口题解:替换k个字符后的最长连续相同字符子串(LeetCode 424)
c语言·数据结构·字符串·滑动窗口·时间复杂度·频率统计·算法题
程序员阿鹏6 小时前
简单介绍一下软引用
java·开发语言·jvm·数据结构·后端
爱奥尼欧8 小时前
【C++】map、set 的底层是什么?红黑树五条性质、插入旋转到模拟实现
开发语言·数据结构·c++
动词ing8 小时前
【题目练习】动态规划+背包问题
数据结构·目标检测
杜 硕8 小时前
单链表经典算法题
数据结构·算法
daxiangxm9 小时前
【趣味休息】“围住小猫在线玩”-“困住小猫”,又回来了
数据结构·人工智能·vscode·github·php
我不会起名字3229 小时前
一天一道力扣Hot100(37):深度优先算法--括号生成
java·数据结构·c++·后端·python·算法·go