1. 简介

跳表(Skip List)是一种基于概率的链表结构,它提供了对平衡树类似的时间复杂度,但实现更简单、更直观。跳表通过在链表中构建多层索引,使得查找、插入、删除等操作的平均时间复杂度为 O(log n)

跳表的三个核心操作是:

✅ 插入
✅ 查找
✅ 删除

我们将通过图示和伪代码来说明这些操作是如何工作的。


2. 动机

想象一个有序单链表,如果我们想查找一个元素,最坏情况下需要遍历所有节点(O(n)):

one level

如果我们给每隔一个节点添加一个“跳跃”指针,跳过两个节点,就可以减少查找时的访问次数:

two levels

进一步地,如果我们给每隔四个节点添加一个指针,访问次数会进一步减少。**如果每隔 2^k 个节点设一个跳跃指针,那么查找复杂度可以降到 O(log n)**。

跳表正是基于这个思想设计的。


3. 跳表作为概率结构

跳表的关键在于节点的层级是随机决定的,而不是固定分配。这样做的好处是:

✅ 插入和删除操作只需局部修改指针
✅ 不需要像平衡树那样频繁调整结构

3.1 层级的设置

我们可以使用如下伪代码随机决定一个节点的层级:

algorithm RandomLevel(p, maxLevel):
    level <- 1
    while (RANDOM() <= p) and (level < maxLevel):
        level <- level + 1
    return level

其中:

  • p 是每次提升层级的概率(通常设为 1/2
  • maxLevel 是允许的最大层级(可以设为无限)

3.2 平均层级

节点层级减1服从几何分布,因此期望值为:

$$ \mathbb{E}[level] = \frac{1}{1 - p} $$

例如,当 p = 1/2 时,平均层级为 2

3.3 指针数量分析

跳表中指针的总数期望为:

$$ n(1 + p) $$

由于 p 是常数,所以跳表的空间复杂度为 O(n),与普通链表相同。

3.4 节点与跳表结构定义

每个节点包含:

  • value:节点值
  • successors[]:指向不同层级后继节点的数组

跳表结构包含:

  • maxLevel:当前跳表的最大层级
  • heads[]:各层级头节点的指针数组

4. 插入操作

插入一个值为 v 的节点需要完成以下步骤:

  1. 查找插入位置
  2. 随机决定节点层级
  3. 更新各层级指针

4.1 伪代码

algorithm InsertIntoSkipList(head, v, maxLevel):
    level <- RandomLevel(maxLevel)
    if level > list.maxLevel:
        for k <- list.maxLevel + 1 to level:
            list.heads[k] <- make an empty node (NULL)
        list.maxLevel <- level

    update <- make an empty array with level reserved elements
    y <- make a node whose value is v

    for k <- list.maxLevel down to 1:
        x <- list.heads[k]
        if x is empty:
            list.heads[k] <- y
        else:
            z <- x.successors[k]
            while (z != NULL) and (v > z.value):
                x <- z
                z <- z.pointers[k]
            x.successors[k] <- y
            y.successors[k] <- z

4.2 示例

插入 17 后跳表的变化如下(新节点为红色):

example after insertion

4.3 时间复杂度

插入操作的时间复杂度主要由查找决定,因此是 O(log n)(期望值)。


5. 查找操作

查找操作与插入类似,但不修改指针。从顶层头节点开始,逐步向下查找直到找到目标值或确定不存在。

5.1 伪代码

algorithm Search(list, v):
    x <- list.heads[list.maxLevel]

    for k <- list.maxLevel to 1:
        if x = NULL:
            x <- list.heads[k]

        while x.successors[k] != NULL and x.successors[k].value < v:
            x <- x.successors[k]

    if x.successors[1] != NULL and x.successors[1].value = v:
        return x.successors[1]
    else:
        return failure

5.2 示例

查找 17 的过程如下:

search

5.3 时间复杂度分析

跳表查找路径的期望长度为:

$$ O\left( \frac{\ell}{p} + p^{\ell}n \right) $$

若选择合适的 (如 log n),该复杂度可降为 O(log n)


6. 删除操作

删除操作也类似插入和查找:

  1. 查找目标节点
  2. 记录指向它的节点
  3. 修改指针,跳过目标节点
  4. 若顶层节点为空,降低 maxLevel

6.1 伪代码

algorithm Delete(list, v):
    update <- make an empty array with list.maxLevel NULL elements

    for k <- list.maxLevel down to 1:
        x <- list.heads[k]
        y <- x.successors[k]

        while y != NULL and v > y.value:
            x <- y
            y <- x.successors[k]

        if y != NULL:
            if y.value = v:
                update[k] <- x

    if y.value = v:
        for k <- 1 to list.maxLevel:
            if update[k] != NULL:
                update[k].successors[k] <- y.successors[k]

        while list.maxLevel > 1 and list.heads[list.maxLevel] = NULL:
            list.maxLevel <- list.maxLevel - 1

6.2 时间复杂度

删除操作同样由查找主导,因此也为 O(log n)(期望值)。


7. 总结

跳表是一种基于概率的高效链表结构,具有以下优点:

✅ 实现简单
✅ 插入、查找、删除平均时间复杂度为 O(log n)
✅ 空间复杂度为 O(n)
❌ 最坏情况复杂度为 O(n),但概率极低

跳表在实际中广泛使用,例如 Java 中的 ConcurrentSkipListMap 和 Redis 的有序集合(ZSet)底层实现。

如果你追求性能与实现简洁的平衡,跳表是一个非常值得掌握的数据结构。


原始标题:The Skip List Data Structure