跳转至

跳表

概述

跳表(Skip List)是由William Pugh于1990年提出的一种基于有序链表的概率数据结构。通过在有序链表上建立多层索引,实现对数级别的查找效率,同时保持实现简单的优点。

跳表的重要性

跳表是Redis有序集合(ZSET)和LevelDB MemTable的底层实现。它在保持平衡树O(log n)查找效率的同时,实现更简单,并发支持更好,是工程实践中非常成功的数据结构。

跳表的设计思想

从有序链表说起

普通有序链表查找需要O(n)时间:

有序链表

1
3
5
7
9
11
13
15
17
19
查找13需要遍历: 135791113 (7次比较)

建立索引加速

借鉴二分查找思想,建立多级索引:

graph LR
    subgraph "跳表结构示意"
        L3["Level 3:     1---------------------> 13"]
        L2["Level 2:     1-----------> 7-------> 13"]
        L1["Level 1:     1-----> 4----> 7-------> 13-----> 17"]
        L0["Level 0:     1-> 3-> 4-> 5-> 7-> 9-> 11-> 13-> 15-> 17-> 19"]
    end
    
    L3 --> L2 --> L1 --> L0
Text Only
完整跳表结构:

Level 3:     1 ────────────────────────→ 13 ────────────→ NULL
             │                           │
Level 2:     1 ───────────→ 7 ─────────→ 13 ────────────→ NULL
             │             │             │
Level 1:     1 ────→ 4 ───→ 7 ─────────→ 13 ────→ 17 ───→ NULL
             │       │     │             │       │
Level 0:     1 → 3 → 4 → 5 → 7 → 9 → 11 → 13 → 15 → 17 → 19 → NULL
            头节点

查找13的过程:
1. Level 3: 1 → 13 (找到!)  仅需2次比较

✓ 查找优化效果

原链表: 7次比较
跳表: 仅需2次比较
提升: 3.5倍

查找效率分析

查找效率推导

假设每层节点数是下一层的1/2:

Level 0: n 个节点

Level 1: n/2 个节点

Level 2: n/4 个节点

...

Level k: n/2^k 个节点

当 n/2^k = 1 时,k = log₂n

查找时,每层最多跳过2个节点

共需比较 2 × log₂n

时间复杂度: O(log n)

跳表结构详解

多层索引结构

graph TB
    subgraph "跳表的节点和指针"
        A["节点1<br/>key=1<br/>forward: [→, →, →]"]
        B["节点7<br/>key=7<br/>forward: [→, →]"]
        C["节点13<br/>key=13<br/>forward: [→, →, →]"]
    end
    
    A -->|"forward[0]"| D["节点3"]
    A -->|"forward[1]"| B
    A -->|"forward[2]"| C
    
    style A fill:#E3F2FD
    style B fill:#E8F5E9
    style C fill:#FFF3E0

随机层数机制

跳表通过随机决定节点层数来维持平衡:

graph TB
    A["插入新节点"] --> B["随机生成层数"]
    B --> C{"以概率P=0.5<br/>继续增加层数?"}
    C -->|"是"| C
    C -->|"否"| D["确定最终层数"]
    
    style D fill:#E8F5E9
Text Only
层数概率分布:

P(层数 = 0) = 1 - P = 0.5
P(层数 = 1) = P × (1 - P) = 0.25
P(层数 = 2) = P² × (1 - P) = 0.125
P(层数 = k) = P^k × (1 - P)

期望层数 = 1 / (1 - P) = 2 (当P=0.5时)

每个节点的期望指针数 = 1 + 1/(1-P) = 3
空间复杂度: O(n)

跳表实现

节点与数据结构定义

C
#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define MAX_LEVEL 16    // 最大层数
#define P 0.5           // 概率因子

// 跳表节点
typedef struct SkipNode {
    int key;                    // 键值
    int value;                  // 关联值
    struct SkipNode **forward;  // 各层前向指针数组
} SkipNode;

// 跳表结构
typedef struct {
    int level;          // 当前最大层数
    SkipNode *header;   // 头节点
} SkipList;

// 创建新节点
SkipNode* createNode(int level, int key, int value) {
    SkipNode *node = (SkipNode*)malloc(sizeof(SkipNode));
    node->key = key;
    node->value = value;
    // 分配level+1层指针(从0到level)
    node->forward = (SkipNode**)calloc(level + 1, sizeof(SkipNode*));
    return node;
}

// 创建跳表
SkipList* createSkipList() {
    SkipList *list = (SkipList*)malloc(sizeof(SkipList));
    list->level = 0;
    // 头节点拥有所有层
    list->header = createNode(MAX_LEVEL, -1, -1);
    
    for (int i = 0; i <= MAX_LEVEL; i++) {
        list->header->forward[i] = NULL;
    }
    
    srand(time(NULL));
    return list;
}

随机层数生成

C
// 生成随机层数
int randomLevel() {
    int level = 0;
    // 以概率P继续增加层数
    while ((double)rand() / RAND_MAX < P && level < MAX_LEVEL) {
        level++;
    }
    return level;
}

/*
随机层数示意:

rand() = 0.3 → 0.3 < 0.5 → level = 1
rand() = 0.7 → 0.7 >= 0.5 → 停止
最终层数: 1

rand() = 0.3 → 0.3 < 0.5 → level = 1
rand() = 0.4 → 0.4 < 0.5 → level = 2
rand() = 0.8 → 0.8 >= 0.5 → 停止
最终层数: 2
*/

查找操作详解

graph TB
    subgraph "查找key=13的过程"
        A["从最高层Level 3开始"] --> B["1 < 13, 前进到13"]
        B --> C["13 == 13, 找到!"]
    end
    
    subgraph "查找key=9的过程"
        D["Level 3: 1 → 13 > 9, 停在1"] --> E["Level 2: 1 → 7 < 9, 继续"]
        E --> F["Level 2: 7 → 13 > 9, 停在7"]
        F --> G["Level 1: 7 → 13 > 9, 停在7"]
        G --> H["Level 0: 7 → 9 == 9, 找到!"]
    end
    
    style C fill:#E8F5E9
    style H fill:#E8F5E9
C
// 查找元素
SkipNode* search(SkipList *list, int key) {
    SkipNode *curr = list->header;
    
    // 从最高层向下查找
    for (int i = list->level; i >= 0; i--) {
        // 在当前层向右移动,直到找到 >= key 的位置
        while (curr->forward[i] && curr->forward[i]->key < key) {
            curr = curr->forward[i];
        }
    }
    
    // 移动到第0层的下一个节点
    curr = curr->forward[0];
    
    // 检查是否找到
    if (curr && curr->key == key) {
        return curr;
    }
    
    return NULL;
}

/*
查找key=9示例:

初始: curr = header

Level 3: header.forward[3] = 节点1
         1.forward[3] = 节点13, 13.key=13 >= 9
         停在节点1

Level 2: 1.forward[2] = 节点7, 7.key=7 < 9
         移动到节点7
         7.forward[2] = 节点13, 13.key=13 >= 9
         停在节点7

Level 1: 7.forward[1] = 节点13, 13.key=13 >= 9
         停在节点7

Level 0: 7.forward[0] = 节点9
         移动到节点9

返回: 节点9
*/

插入操作详解

graph TB
    subgraph "插入key=6的过程"
        A["1. 查找插入位置,记录update数组"] --> B["2. 随机生成层数,假设为2"]
        B --> C["3. 创建新节点,连接各层指针"]
        C --> D["4. 更新update节点的forward指针"]
    end
    
    style D fill:#E8F5E9
Text Only
插入key=6示意:

插入前:
Level 2:     1 ───────────→ 7
Level 1:     1 ────→ 4 ───→ 7
Level 0:     1 → 3 → 4 → 5 → 7

查找位置后的update数组:
update[2] = 节点1
update[1] = 节点4
update[0] = 节点5

假设随机层数为2:
新节点6的forward:
forward[2] = NULL
forward[1] = 节点7
forward[0] = 节点7

更新后:
Level 2:     1 ───────→ 6 ─→ 7
Level 1:     1 ────→ 4 → 6 → 7
Level 0:     1 → 3 → 4 → 5 → 6 → 7
C
// 插入元素
void insert(SkipList *list, int key, int value) {
    SkipNode *update[MAX_LEVEL + 1];  // 记录各层的前驱节点
    SkipNode *curr = list->header;
    
    // 查找插入位置,记录每层的前驱节点
    for (int i = list->level; i >= 0; i--) {
        while (curr->forward[i] && curr->forward[i]->key < key) {
            curr = curr->forward[i];
        }
        update[i] = curr;  // 记录当前层的前驱
    }
    
    curr = curr->forward[0];
    
    // 如果key已存在,更新value
    if (curr && curr->key == key) {
        curr->value = value;
        return;
    }
    
    // 随机生成新节点层数
    int level = randomLevel();
    
    // 如果新层数大于当前最大层数,初始化高层
    if (level > list->level) {
        for (int i = list->level + 1; i <= level; i++) {
            update[i] = list->header;
        }
        list->level = level;
    }
    
    // 创建新节点
    SkipNode *newNode = createNode(level, key, value);
    
    // 插入新节点,更新各层指针
    for (int i = 0; i <= level; i++) {
        newNode->forward[i] = update[i]->forward[i];
        update[i]->forward[i] = newNode;
    }
}

删除操作

C
// 删除元素
void delete(SkipList *list, int key) {
    SkipNode *update[MAX_LEVEL + 1];
    SkipNode *curr = list->header;
    
    // 查找删除位置
    for (int i = list->level; i >= 0; i--) {
        while (curr->forward[i] && curr->forward[i]->key < key) {
            curr = curr->forward[i];
        }
        update[i] = curr;
    }
    
    curr = curr->forward[0];
    
    // 如果没找到,直接返回
    if (!curr || curr->key != key) {
        return;
    }
    
    // 更新各层指针
    for (int i = 0; i <= list->level; i++) {
        if (update[i]->forward[i] != curr) {
            break;
        }
        update[i]->forward[i] = curr->forward[i];
    }
    
    // 降低层数
    while (list->level > 0 && list->header->forward[list->level] == NULL) {
        list->level--;
    }
    
    // 释放节点内存
    free(curr->forward);
    free(curr);
}

范围查询

C
// 范围查询 [minKey, maxKey]
void rangeQuery(SkipList *list, int minKey, int maxKey) {
    SkipNode *curr = list->header;
    
    // 定位到 >= minKey 的位置
    for (int i = list->level; i >= 0; i--) {
        while (curr->forward[i] && curr->forward[i]->key < minKey) {
            curr = curr->forward[i];
        }
    }
    
    curr = curr->forward[0];
    
    // 在底层遍历范围内的元素
    printf("范围查询 [%d, %d]: ", minKey, maxKey);
    while (curr && curr->key <= maxKey) {
        printf("(%d, %d) ", curr->key, curr->value);
        curr = curr->forward[0];
    }
    printf("\n");
}

C++ 实现

C++
#include <vector>
#include <random>
#include <iostream>

template<typename K, typename V>
class SkipList {
private:
    static const int MAX_LEVEL = 16;
    static constexpr double P = 0.5;
    
    struct Node {
        K key;
        V value;
        std::vector<Node*> forward;
        
        Node(K k, V v, int level) 
            : key(k), value(v), forward(level + 1, nullptr) {}
    };
    
    Node *header;
    int level;
    std::mt19937 rng;
    std::uniform_real_distribution<double> dist;
    
    int randomLevel() {
        int lvl = 0;
        while (dist(rng) < P && lvl < MAX_LEVEL) {
            lvl++;
        }
        return lvl;
    }
    
public:
    SkipList() : level(0), rng(std::random_device{}()), dist(0, 1) {
        header = new Node(K{}, V{}, MAX_LEVEL);
    }
    
    ~SkipList() {
        Node *curr = header->forward[0];
        while (curr) {
            Node *temp = curr;
            curr = curr->forward[0];
            delete temp;
        }
        delete header;
    }
    
    // 查找
    V* search(const K& key) {
        Node *curr = header;
        
        for (int i = level; i >= 0; i--) {
            while (curr->forward[i] && curr->forward[i]->key < key) {
                curr = curr->forward[i];
            }
        }
        
        curr = curr->forward[0];
        return (curr && curr->key == key) ? &curr->value : nullptr;
    }
    
    // 插入
    void insert(const K& key, const V& value) {
        std::vector<Node*> update(MAX_LEVEL + 1, nullptr);
        Node *curr = header;
        
        for (int i = level; i >= 0; i--) {
            while (curr->forward[i] && curr->forward[i]->key < key) {
                curr = curr->forward[i];
            }
            update[i] = curr;
        }
        
        curr = curr->forward[0];
        
        if (curr && curr->key == key) {
            curr->value = value;
            return;
        }
        
        int lvl = randomLevel();
        
        if (lvl > level) {
            for (int i = level + 1; i <= lvl; i++) {
                update[i] = header;
            }
            level = lvl;
        }
        
        Node *newNode = new Node(key, value, lvl);
        
        for (int i = 0; i <= lvl; i++) {
            newNode->forward[i] = update[i]->forward[i];
            update[i]->forward[i] = newNode;
        }
    }
    
    // 删除
    bool remove(const K& key) {
        std::vector<Node*> update(MAX_LEVEL + 1, nullptr);
        Node *curr = header;
        
        for (int i = level; i >= 0; i--) {
            while (curr->forward[i] && curr->forward[i]->key < key) {
                curr = curr->forward[i];
            }
            update[i] = curr;
        }
        
        curr = curr->forward[0];
        
        if (!curr || curr->key != key) {
            return false;
        }
        
        for (int i = 0; i <= level; i++) {
            if (update[i]->forward[i] != curr) break;
            update[i]->forward[i] = curr->forward[i];
        }
        
        delete curr;
        
        while (level > 0 && header->forward[level] == nullptr) {
            level--;
        }
        
        return true;
    }
    
    // 打印结构
    void print() {
        for (int i = level; i >= 0; i--) {
            std::cout << "Level " << i << ": ";
            Node *curr = header->forward[i];
            while (curr) {
                std::cout << curr->key << " ";
                curr = curr->forward[i];
            }
            std::cout << std::endl;
        }
    }
};

复杂度分析

时间复杂度

操作 平均时间复杂度 最坏时间复杂度 说明
查找 O(log n) O(n) 最坏情况所有节点都在第0层
插入 O(log n) O(n) 包含查找和指针更新
删除 O(log n) O(n) 包含查找和指针更新
范围查询 O(log n + k) O(n) k为结果数量

空间复杂度

Text Only
每个节点的期望指针数 = 1 + 1/(1-P) = 1 + 2 = 3 (当P=0.5)

总空间 = n × 平均指针数 = O(n)

详细计算:
E[指针数] = Σ(k=0 to ∞) P(层数≥k)
         = Σ(k=0 to ∞) P^k
         = 1/(1-P)
         = 2 (当P=0.5)

每个节点平均: 1个key + 1个value + 2个forward指针 = O(1)
总空间: O(n)

跳表 vs 平衡树对比

特性 跳表 平衡树(AVL/红黑树)
查找效率 O(log n) O(log n)
插入效率 O(log n) O(log n)
删除效率 O(log n) O(log n)
实现难度 简单 复杂 ✗
范围查询 高效 需要中序遍历
并发支持 容易 困难 ✗
空间开销 较大 (O(n)×3) 较小 (O(n))
有序遍历 直接遍历第0层 中序遍历
最坏保证 概率保证 确定性保证

跳表的优势

1. 实现简单

Text Only
1
2
3
4
5
6
7
8
9
平衡树需要:
- 复杂的旋转操作
- 多种情况的分类讨论
- 维护平衡因子/颜色

跳表只需要:
- 简单的指针操作
- 随机层数生成
- 无需复杂的平衡逻辑

2. 范围查询高效

C
// 跳表:直接在第0层遍历
void rangeQuery(int min, int max) {
    Node *curr = findPosition(min);  // O(log n)
    while (curr->key <= max) {       // O(k)
        process(curr);
        curr = curr->forward[0];
    }
}

// 平衡树:需要中序遍历,更复杂

3. 并发友好

Text Only
1
2
3
4
5
6
7
8
跳表并发优势:
- 插入/删除只影响局部节点
- 无需全局重平衡(旋转)
- 易于实现无锁版本

平衡树并发困难:
- 旋转可能影响大量节点
- 需要复杂的锁策略

应用场景

1. Redis 有序集合(ZSET)

C
1
2
3
4
5
6
7
// Redis ZSET 使用跳表实现
// 支持的操作:
ZADD key score member     // 插入/更新
ZREM key member           // 删除
ZRANK key member          // 获取排名
ZRANGE key start stop     // 范围查询
ZSCORE key member         // 获取分数

2. LevelDB MemTable

C
1
2
3
4
5
// LevelDB 内存表使用跳表
// 特点:
// - 内存中的有序键值存储
// - 快速查找和范围扫描
// - 写入效率高

3. 时间序列数据

C
1
2
3
4
5
// 存储带时间戳的数据
SkipList<Timestamp, Data> timeSeries;

// 高效查询时间范围
timeSeries.rangeQuery(startTime, endTime);

跳表变体

1. 确定性跳表

使用确定性方法决定层数,而非随机。

2. 并发跳表

无锁或细粒度锁的并发实现。

C++
1
2
3
4
// 无锁跳表核心思想:
// 1. 使用CAS操作更新指针
// 2. 节点标记删除而非物理删除
// 3. 延迟删除无效节点

3. 可持久化跳表

支持访问历史版本。

参考资料