跳表(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.5 或 p = 0.25。p 越小,高层节点越少,空间越省,但查找稍慢。

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. 选择合适的 p 和 MAX_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:ConcurrentSkipListMap、ConcurrentSkipListSet;
    • Redis:ZSET;
    • LevelDB / RocksDB:MemTable;
    • Python:可使用 Redis,或第三方有序容器库。

十一、总结

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

它的核心优势是:

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

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

相关推荐
flinn_small2 小时前
三分算法:原理、实现与实战应用
数据结构·算法·三分
INGNIGHT3 小时前
944 · 最大子矩阵(前缀和)
数据结构·c++·算法
All for pursuit.4 小时前
【动态规划-6】96.不同的二叉搜索树
数据结构·c++·算法·leetcode·动态规划
信奥卷王4 小时前
[GESP202609 六级] 数组划分
数据结构·算法
All for pursuit.5 小时前
【并查集-2】399.除法求值
数据结构·c++·算法·leetcode
殷色玫瑰8 小时前
AVL树:从平衡因子到四种旋转的完整实现
c语言·数据结构·c++·算法·visualstudio
y1su9 小时前
Leetcode 二分模板
java·数据结构·算法·leetcode·排序算法
bksczm9 小时前
数据结构的大纲概括
数据结构
千千寰宇10 小时前
[数据库系统] 数据库系统的设计原理研究
数据结构·数据库·database-opengemini/influxdb·database-dament/达梦
殷色玫瑰11 小时前
C++ STL:map 与 set 详解——从底层红黑树到实际应用
c语言·数据结构·c++·算法·visualstudio·rpc