跳表(Skip List):简单高效的有序数据结构

20 阅读13分钟

目录

  1. 跳表的起源与动机
  2. 核心思想与直观理解
  3. 数据结构详解
  4. 核心操作实现
  5. 复杂度分析
  6. 与其他数据结构的对比
  7. 实际应用场景
  8. 高级主题

1. 跳表的起源与动机

1.1 历史背景

跳表由William Pugh于1990年在论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》中提出。Pugh当时面临一个经典问题:

如何在保持链表简单性的同时,获得接近二叉搜索树的查询效率?

传统解决方案要么复杂(AVL树、红黑树的旋转平衡),要么牺牲有序性(哈希表)。跳表提供了一个优雅的折中:概率性平衡代替强制性平衡

1.2 设计哲学

数据结构平衡策略复杂度实现难度
有序链表查询O(n)极简单
二叉搜索树查询O(n)(退化)简单
AVL树强制旋转平衡查询O(log n)复杂
跳表概率性分层期望O(log n)简单

跳表的核心洞察:随机化可以替代复杂的再平衡操作


2. 核心思想与直观理解

2.1 从链表到跳表

想象一条高速公路(链表)和多条快速路(索引层):

普通公路(Level 0):每1公里一个出口
    ├───●───●───●───●───●───●───●───●───●───●───●───●───●───●───●
       1   2   3   4   5   6   7   8   9  10  11  12  13  14  15

一级快速路(Level 1):每2公里一个出口
    ├───●───────────●───────────●───────────●───────────●
       1           3           5           7           9

二级快速路(Level 2):每4公里一个出口
    ├───●───────────────────────●───────────────────────●
       1                       5                       9

三级快速路(Level 3):每8公里一个出口
    ├───●───────────────────────────────────────────────●
       1                                               9

从A地到B地,你先走高级快速路,接近目的地时逐级下降,最终到达。

2.2 关键特性

特性1:有序性

  • 底层(Level 0)是有序链表,所有数据按序排列

特性2:索引分层

  • 每个节点随机拥有1~MAX_LEVEL层指针
  • 上层节点是下层节点的"快速通道"

特性3:概率分布

  • 节点出现在第k层的概率为 p^k(通常p=0.5)
  • 50%的节点有1层,25%有2层,12.5%有3层...
节点层数分布(p=0.5):
Level 0: ████████████████████████████████████████ 100%
Level 1: ████████████████████░░░░░░░░░░░░░░░░░░░░  50%
Level 2: ██████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  25%
Level 3: █████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  12.5%
Level 4: ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   6.25%

3. 数据结构详解

3.1 节点结构

from typing import List, Optional
import random

class SkipListNode:
    """
    跳表节点
    
    每个节点包含:
    - key: 排序键(可为None表示头节点)
    - value: 存储的值
    - forward: 各层的前向指针数组
    """
    def __init__(self, key: Optional[int], value: Optional[any], level: int):
        self.key = key
        self.value = value
        # forward[i] 表示第i层的下一个节点
        self.forward: List[Optional['SkipListNode']] = [None] * level
    
    def __repr__(self):
        return f"Node({self.key}: {self.value}, levels={len(self.forward)})"

3.2 跳表结构

class SkipList:
    """
    跳表实现
    
    参数:
    - max_level: 最大层数,决定最高索引层(通常16足够处理2^16个元素)
    - p: 晋升概率,控制层数分布(通常0.5)
    """
    def __init__(self, max_level: int = 16, p: float = 0.5):
        self.max_level = max_level
        self.p = p
        self.level = 0  # 当前实际使用的最高层数
        
        # 头节点,包含所有层的起始指针
        self.head = SkipListNode(None, None, max_level)
        
        # 统计信息
        self.size = 0
    
    def _random_level(self) -> int:
        """
        随机生成节点层数
        
        算法:每层以概率p晋升,直到不晋升或达到max_level
        
        数学期望:E[level] = 1/(1-p) - 1
        当p=0.5时,期望层数为1
        """
        level = 1
        while random.random() < self.p and level < self.max_level:
            level += 1
        return level

3.3 可视化表示

跳表示例(插入 3, 6, 7, 9, 12, 17, 19, 21, 25, 26):

Level 3:  HEAD ──────────────────────────────────────────────→ 25
Level 2:  HEAD ─────────────────────────────→ 17 ─────────────→ 25 ─────────────→ 26
Level 1:  HEAD ───────────→ 6 ───────────→ 17 ───────────→ 25 ───────────→ 26
Level 0:  HEAD  3  6  7  9  12  17  19  21  25  26

查找路径示例(查找 19):
1. L3: HEAD  25 (25>19,下降)
2. L2: HEAD  17 (17<19,前进)  25 (25>19,下降)
3. L1: 17  25 (25>19,下降)
4. L0: 17  19 (找到!)

实际访问节点:HEAD(L3)  HEAD(L2)  17(L2)  17(L1)  17(L0)  19(L0)
共6步,而普通链表需要7步

4. 核心操作实现

4.1 查询操作(Search)

算法思路:从最高层开始,在当前层尽可能向右移动,遇到更大的值就下降一层。

def search(self, key: int) -> Optional[any]:
    """
    查询操作
    
    步骤:
    1. 从当前最高层(level)开始
    2. 在当前层向右移动,直到下一个节点key >= target
    3. 下降到下一层,重复步骤2
    4. 到达Level 0后,检查下一个节点是否等于target
    
    时间复杂度:O(log n) 期望
    """
    current = self.head
    
    # 从高层到低层
    for i in range(self.level - 1, -1, -1):
        # 在当前层尽可能向右走
        while (current.forward[i] is not None and 
               current.forward[i].key < key):
            current = current.forward[i]
    
    # 现在current是小于key的最大节点,检查下一个
    current = current.forward[0]
    
    if current is not None and current.key == key:
        return current.value
    return None

查询过程可视化

查找 key = 15

Level 2:  1 ──────────────→ 9 ──────────────→ 17
          ↑                 ↑
          │                 └─ next=17 > 15, 下降
          └─ current

Level 1:  1 ──────→ 5 ──────→ 9 ──────→ 13 ──────→ 17
                            ↑           ↑
                            │           └─ next=17 > 15, 下降
                            └─ current (从L2下来)

Level 0:  1 → 3 → 5 → 7 → 9 → 11 → 13 →→ 17
                                    ↑
                                    └─ next=15 == target, 找到!

访问节点数:3层 × 少量节点 = O(log n)

4.2 插入操作(Insert)

算法思路

  1. 找到每一层需要插入位置的前驱节点(update数组)
  2. 随机决定新节点层数
  3. 在各层插入新节点,调整指针
def insert(self, key: int, value: any) -> bool:
    """
    插入操作
    
    步骤:
    1. 找到每一层的前驱节点(update数组)
    2. 如果key已存在,更新value
    3. 随机决定新节点层数
    4. 如果新层数>当前层数,更新head的forward和level
    5. 创建新节点,在各层插入
    
    时间复杂度:O(log n) 期望
    """
    # update[i] 表示第i层需要更新的节点(即新节点的前驱)
    update: List[Optional[SkipListNode]] = [None] * self.max_level
    current = self.head
    
    # 第一步:找到每一层的前驱节点
    for i in range(self.level - 1, -1, -1):
        while (current.forward[i] is not None and 
               current.forward[i].key < key):
            current = current.forward[i]
        update[i] = current
    
    # 移动到Level 0的下一个节点
    current = current.forward[0]
    
    # Key已存在,更新value
    if current is not None and current.key == key:
        current.value = value
        return False  # 未插入新节点
    
    # 第二步:随机决定新节点层数
    new_level = self._random_level()
    
    # 如果新层数超过当前层数,需要更新更高层的update为head
    if new_level > self.level:
        for i in range(self.level, new_level):
            update[i] = self.head
        self.level = new_level
    
    # 第三步:创建新节点
    new_node = SkipListNode(key, value, new_level)
    
    # 第四步:在各层插入(调整指针)
    for i in range(new_level):
        new_node.forward[i] = update[i].forward[i]
        update[i].forward[i] = new_node
    
    self.size += 1
    return True

插入过程可视化

插入 key=10, value="X"(随机层数=2)

插入前:
Level 2:  HEAD ─────────────────────────→ 17
Level 1:  HEAD ───────────→ 9 ─────────→ 17
Level 0:  HEAD  3  6  9  12  17

Step 1: 找到update数组
  L2: HEAD (因为17>10)
  L1: 9 (因为9<10<17)
  L0: 9 (因为9<10<12)

Step 2: 随机层数=2,需要更新L0,L1

Step 3: 插入(调整指针)
Level 2:  HEAD ─────────────────────────→ 17  (不变)
Level 1:  HEAD ───────────→ 9 ─────→ 17
                              
                            next(17)
Level 0:  HEAD  3  6  9 ────→ 12  17
                          
                        next(12)

新节点10的forward: [12, 17, None, None, ...]
9的forward[0]从12改为10,forward[1]从17改为10

4.3 删除操作(Delete)

def delete(self, key: int) -> bool:
    """
    删除操作
    
    步骤:
    1. 找到每一层的前驱节点
    2. 如果目标不存在,返回False
    3. 在各层删除目标节点(调整指针跳过目标)
    4. 更新当前层数(去掉空的顶层)
    
    时间复杂度:O(log n) 期望
    """
    update: List[Optional[SkipListNode]] = [None] * self.max_level
    current = self.head
    
    # 找到每一层的前驱
    for i in range(self.level - 1, -1, -1):
        while (current.forward[i] is not None and 
               current.forward[i].key < key):
            current = current.forward[i]
        update[i] = current
    
    # 目标节点
    target = current.forward[0]
    
    # 不存在
    if target is None or target.key != key:
        return False
    
    # 在各层删除(调整指针)
    for i in range(self.level):
        if update[i].forward[i] != target:
            break  # 更高层没有该节点
        update[i].forward[i] = target.forward[i]
    
    # 更新当前层数
    while (self.level > 0 and 
           self.head.forward[self.level - 1] is None):
        self.level -= 1
    
    self.size -= 1
    return True

4.4 范围查询(Range Query)

跳表的优势之一,哈希表无法高效支持:

def range_query(self, start: int, end: int) -> List[tuple]:
    """
    范围查询:返回key在[start, end]内的所有(key, value)
    
    时间复杂度:O(log n + k),k为结果数量
    """
    result = []
    
    # 找到start位置
    current = self.head
    for i in range(self.level - 1, -1, -1):
        while (current.forward[i] is not None and 
               current.forward[i].key < start):
            current = current.forward[i]
    
    # 从start开始遍历Level 0
    current = current.forward[0]
    while current is not None and current.key <= end:
        result.append((current.key, current.value))
        current = current.forward[0]
    
    return result

5. 复杂度分析

5.1 数学推导

节点层数期望

  • 第k层节点数期望:n × p^k
  • 当 p=0.5 时,第k层约有 n/2^k 个节点

查询复杂度

  • 每层访问的节点数期望:2(因为节点密度减半)
  • 总层数期望:log_{1/p}(n)
  • 总复杂度:O(log n)

空间复杂度

  • 每个节点期望层数:1/(1-p)
  • 当p=0.5时,期望2层
  • 总空间:O(n)

5.2 概率分析表(p=0.5, n=1,000,000)

层数期望节点数查询时访问概率
1500,000100%
2250,000~100%
3125,000~50%
462,500~25%
531,250~12%
.........
20<1极低

5.3 与理论最优的比较

操作跳表期望平衡树有序数组
查询O(log n)O(log n)O(log n)
插入O(log n)O(log n)O(n)
删除O(log n)O(log n)O(n)
范围查询O(log n + k)O(log n + k)O(log n + k)
实现难度简单复杂中等

6. 与其他数据结构的对比

6.1 详细对比表

特性跳表红黑树AVL树B+树哈希表
有序遍历✅ O(n)✅ O(n)✅ O(n)✅ O(n)❌ O(n log n)
最值查询✅ O(1)✅ O(log n)✅ O(log n)✅ O(log n)❌ O(n)
范围查询✅ O(log n+k)✅ O(log n+k)✅ O(log n+k)✅ O(log n+k)❌ O(n)
第k大元素✅ O(log n)✅ O(log n)*✅ O(log n)*✅ O(log n)❌ O(n)
实现复杂度
常数因子较小较小较小较大最小
锁粒度(并发)

*需要维护子树大小

6.2 为什么选择跳表?

优势场景

  1. 需要有序性 + 频繁修改:跳表优于有序数组
  2. 实现简单:跳表代码量约为红黑树的1/5
  3. 并发性能好:细粒度锁,不同层的操作可并行
  4. 范围查询友好:天然支持,无需额外结构

劣势场景

  1. 极致性能要求:哈希表O(1)更快(但无序)
  2. 内存极度紧张:链表结构有指针开销
  3. 需要持久化:B+树更适合磁盘存储

7. 实际应用场景

7.1 Redis 中的跳表

Redis 的 Sorted Set(ZSET) 底层使用跳表 + 哈希表:

// Redis 跳表节点定义(zskiplistNode)
typedef struct zskiplistNode {
    sds ele;                    // 成员对象
    double score;               // 分数(排序键)
    struct zskiplistNode *backward;  // 后退指针(用于倒序遍历)
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;      // 到下一个节点的跨度(用于RANK查询)
    } level[];
} zskiplistNode;

Redis选择跳表的原因

  1. 实现简单,代码量少(vs 红黑树)
  2. 范围查询天然支持(ZRange/ZRevRange)
  3. 有序遍历效率高
  4. 方便实现ZRANK(通过span字段)

7.2 LevelDB/RocksDB 中的跳表

内存中的 MemTable 使用跳表作为默认实现:

写入流程:
1. 写入WAL日志(持久化)
2. 插入跳表(MemTable)
3. MemTable满后,转为Immutable MemTable
4. 后台线程将Immutable MemTable写入SSTable(磁盘)

跳表的优势

  • 写入无需磁盘IO,纯内存操作
  • 有序性保证后续SSTable合并高效
  • 并发读写性能好(MVCC)

7.3 其他应用

系统用途特点
Lucene倒排索引合并多路归并优化
MemSQL内存索引并发控制
Cassandra内存表写优化
HBaseMemStore类似LevelDB

8. 高级主题

8.1 并发跳表(Lock-Free)

使用 CAS(Compare-And-Swap) 实现无锁并发:

import threading
from typing import Atomic  # 伪代码,实际需使用特定库

class ConcurrentSkipList:
    """
    无锁并发跳表(概念演示)
    
    核心思想:
    1. 使用CAS操作更新指针
    2. 标记删除(逻辑删除)避免ABA问题
    3. 每层独立加锁或使用原子操作
    """
    def __init__(self):
        self.head = SkipListNode(None, None, MAX_LEVEL)
        self.locks = [threading.Lock() for _ in range(MAX_LEVEL)]
    
    def concurrent_insert(self, key, value):
        # 1. 找到位置(无锁遍历)
        # 2. 从底层开始,逐层CAS插入
        # 3. 如果CAS失败,重试
        pass  # 实际实现较复杂

8.2 优化策略

1. 概率p的选择

  • p=0.5:平衡空间和速度(常用)
  • p=0.25:更少指针,更省空间,查询略慢
  • p=0.75:更多指针,更快查询,更费空间

2. 确定性跳表(Deterministic Skip List)

  • 不随机,按固定规则分层(如每2^k个节点升一层)
  • 查询复杂度严格O(log n),但插入可能需要调整多个节点

3. 跳表变种

变种特点应用
1-2-3跳表确定性分层教学演示
指数跳表节点按指数分布特定负载优化
分数级联加速查询计算几何

8.3 完整优化实现

class OptimizedSkipList:
    """
    生产级跳表优化
    """
    def __init__(self, max_level: int = 32, p: float = 0.25):
        self.max_level = max_level
        self.p = p
        self.level = 1
        self.head = self._create_node(None, None, max_level)
        self._size = 0
        
        # 预分配update数组,避免重复创建
        self._update_buffer = [None] * max_level
    
    def _create_node(self, key, value, level):
        # 使用对象池或__slots__减少内存开销
        node = object.__new__(SkipListNode)
        node.key = key
        node.value = value
        node.forward = [None] * level
        return node
    
    def __len__(self):
        return self._size
    
    def iter_range(self, start, end):
        """生成器实现的范围查询,节省内存"""
        node = self._find_ge(start)
        while node and node.key <= end:
            yield (node.key, node.value)
            node = node.forward[0]

总结

跳表是一种简单、高效、优雅的概率性数据结构,它通过随机分层实现了对数级查询复杂度,同时保持了链表的简洁性。其核心优势在于:

  1. 实现简单:无需复杂的旋转平衡逻辑
  2. 性能优异:期望O(log n)的查询/插入/删除
  3. 范围友好:天然支持有序遍历和范围查询
  4. 并发适配:细粒度锁或无锁实现可行

在现代系统中,跳表广泛应用于内存数据库(Redis、LevelDB)的索引实现,是有序数据结构的重要选择。


参考实现代码(完整可运行版本):

点击查看完整代码
import random
from typing import List, Optional, Iterator, Tuple

class SkipListNode:
    __slots__ = ['key', 'value', 'forward']
    
    def __init__(self, key: Optional[int], value: Optional[any], level: int):
        self.key = key
        self.value = value
        self.forward: List[Optional['SkipListNode']] = [None] * level

class SkipList:
    def __init__(self, max_level: int = 16, p: float = 0.5):
        self.max_level = max_level
        self.p = p
        self.level = 0
        self.head = SkipListNode(None, None, max_level)
        self._size = 0
    
    def _random_level(self) -> int:
        level = 1
        while random.random() < self.p and level < self.max_level:
            level += 1
        return level
    
    def search(self, key: int) -> Optional[any]:
        current = self.head
        for i in range(self.level - 1, -1, -1):
            while current.forward[i] and current.forward[i].key < key:
                current = current.forward[i]
        current = current.forward[0]
        return current.value if current and current.key == key else None
    
    def insert(self, key: int, value: any) -> None:
        update: List[Optional[SkipListNode]] = [None] * self.max_level
        current = self.head
        
        for i in range(self.level - 1, -1, -1):
            while current.forward[i] and current.forward[i].key < key:
                current = current.forward[i]
            update[i] = current
        
        current = current.forward[0]
        
        if current and current.key == key:
            current.value = value
            return
        
        new_level = self._random_level()
        if new_level > self.level:
            for i in range(self.level, new_level):
                update[i] = self.head
            self.level = new_level
        
        new_node = SkipListNode(key, value, new_level)
        for i in range(new_level):
            new_node.forward[i] = update[i].forward[i]
            update[i].forward[i] = new_node
        
        self._size += 1
    
    def delete(self, key: int) -> bool:
        update: List[Optional[SkipListNode]] = [None] * self.max_level
        current = self.head
        
        for i in range(self.level - 1, -1, -1):
            while current.forward[i] and current.forward[i].key < key:
                current = current.forward[i]
            update[i] = current
        
        target = current.forward[0]
        if not target or target.key != key:
            return False
        
        for i in range(self.level):
            if update[i].forward[i] != target:
                break
            update[i].forward[i] = target.forward[i]
        
        while self.level > 0 and not self.head.forward[self.level - 1]:
            self.level -= 1
        
        self._size -= 1
        return True
    
    def __iter__(self) -> Iterator[Tuple[int, any]]:
        current = self.head.forward[0]
        while current:
            yield (current.key, current.value)
            current = current.forward[0]
    
    def __len__(self) -> int:
        return self._size
    
    def __repr__(self) -> str:
        items = list(self)
        return f"SkipList({items})"

# 测试
if __name__ == "__main__":
    sl = SkipList()
    for i in [3, 6, 7, 9, 12, 19, 17, 26, 21, 25]:
        sl.insert(i, f"val_{i}")
    
    print("跳表内容:", list(sl))
    print("查询 17:", sl.search(17))
    print("查询 100:", sl.search(100))
    sl.delete(17)
    print("删除 17 后:", list(sl))
    print("长度:", len(sl))