【数据结构与算法】第45篇:跳跃表(Skip List)

一、什么是跳跃表

1.1 为什么需要跳跃表

普通单链表查找元素需要 O(n)。跳跃表通过建立多层索引,让查找可以"跳过"部分节点,达到类似二分查找的效果。

示例:查找 7

text

复制代码
第2层: 1 ----------> 9
第1层: 1 -----> 5 -----> 9
第0层: 1 -> 3 -> 5 -> 7 -> 9

从第2层开始:1 < 7,到9 > 7,下降到第1层
第1层:5 < 7,到9 > 7,下降到第0层
第0层:7 找到

1.2 特点

优点 缺点
平均 O(log n) 查找 空间复杂度 O(n log n)
实现比平衡树简单 最坏情况 O(n)(但概率极低)
支持范围查询 随机数依赖
并发友好 内存开销较大

1.3 应用场景

  • Redis ZSet:有序集合的底层实现

  • LevelDB:LSM 树的 MemTable

  • Lucene:倒排索引的跳跃表优化


二、跳跃表的结构设计

2.1 节点结构

每个节点包含:值、层高、每层的后继指针。

c

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.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 {
    SkipNode *header;                // 头节点(不存储数据)
    int level;                       // 当前最大层数
    int size;                        // 节点个数
} SkipList;

2.2 随机生成层高

c

复制代码
int randomLevel() {
    int level = 0;
    while ((rand() / (double)RAND_MAX) < P && level < MAX_LEVEL - 1) {
        level++;
    }
    return level;
}

2.3 创建节点

c

复制代码
SkipNode* createNode(int key, int value, int level) {
    SkipNode *node = (SkipNode*)malloc(sizeof(SkipNode));
    node->key = key;
    node->value = value;
    node->forward = (SkipNode**)malloc((level + 1) * sizeof(SkipNode*));
    for (int i = 0; i <= level; i++) {
        node->forward[i] = NULL;
    }
    return node;
}

2.4 初始化跳跃表

c

复制代码
SkipList* createSkipList() {
    SkipList *list = (SkipList*)malloc(sizeof(SkipList));
    list->level = 0;
    list->size = 0;
    list->header = createNode(0, 0, MAX_LEVEL);
    return list;
}

三、基本操作实现

3.1 查找

从最高层开始,每层找到最后一个小于 key 的节点,然后下降一层继续。

c

复制代码
int search(SkipList *list, int key, int *value) {
    SkipNode *cur = list->header;
    
    for (int i = list->level; i >= 0; i--) {
        while (cur->forward[i] != NULL && cur->forward[i]->key < key) {
            cur = cur->forward[i];
        }
    }
    
    cur = cur->forward[0];
    if (cur != NULL && cur->key == key) {
        *value = cur->value;
        return 1;
    }
    return 0;
}

3.2 插入

  1. 用 update 数组记录每层插入位置的前驱节点

  2. 随机生成新节点的层高

  3. 如果层高超过当前最大层,更新 update 中超出部分为 header

  4. 在每层插入新节点

c

复制代码
void insert(SkipList *list, int key, int value) {
    SkipNode *update[MAX_LEVEL + 1];
    SkipNode *cur = list->header;
    
    // 1. 找到每层插入位置的前驱
    for (int i = list->level; i >= 0; i--) {
        while (cur->forward[i] != NULL && cur->forward[i]->key < key) {
            cur = cur->forward[i];
        }
        update[i] = cur;
    }
    
    cur = cur->forward[0];
    
    // 如果 key 已存在,更新 value
    if (cur != NULL && cur->key == key) {
        cur->value = value;
        return;
    }
    
    // 2. 随机生成层高
    int newLevel = randomLevel();
    if (newLevel > list->level) {
        for (int i = list->level + 1; i <= newLevel; i++) {
            update[i] = list->header;
        }
        list->level = newLevel;
    }
    
    // 3. 创建新节点并插入
    SkipNode *newNode = createNode(key, value, newLevel);
    for (int i = 0; i <= newLevel; i++) {
        newNode->forward[i] = update[i]->forward[i];
        update[i]->forward[i] = newNode;
    }
    
    list->size++;
}

3.3 删除

  1. 用 update 数组记录每层要删除节点的前驱

  2. 如果找到要删除的节点,在各层将其跳过

c

复制代码
int delete(SkipList *list, int key, int *value) {
    SkipNode *update[MAX_LEVEL + 1];
    SkipNode *cur = list->header;
    
    // 找到每层删除位置的前驱
    for (int i = list->level; i >= 0; i--) {
        while (cur->forward[i] != NULL && cur->forward[i]->key < key) {
            cur = cur->forward[i];
        }
        update[i] = cur;
    }
    
    cur = cur->forward[0];
    
    if (cur == NULL || cur->key != key) {
        return 0;  // 不存在
    }
    
    *value = cur->value;
    
    // 在各层删除节点
    for (int i = 0; i <= list->level; i++) {
        if (update[i]->forward[i] != cur) break;
        update[i]->forward[i] = cur->forward[i];
    }
    
    // 更新最高层数
    while (list->level > 0 && list->header->forward[list->level] == NULL) {
        list->level--;
    }
    
    free(cur->forward);
    free(cur);
    list->size--;
    
    return 1;
}

3.4 遍历(打印所有元素)

c

复制代码
void printSkipList(SkipList *list) {
    printf("跳跃表(共%d个节点,当前最高层=%d):\n", list->size, list->level);
    printf("第0层: ");
    SkipNode *cur = list->header->forward[0];
    while (cur != NULL) {
        printf("(%d:%d) ", cur->key, cur->value);
        cur = cur->forward[0];
    }
    printf("\n");
}

四、完整代码演示

c

复制代码
#include <stdio.h>
#include <stdlib.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 {
    SkipNode *header;
    int level;
    int size;
} SkipList;

int randomLevel() {
    int level = 0;
    while ((rand() / (double)RAND_MAX) < P && level < MAX_LEVEL - 1) {
        level++;
    }
    return level;
}

SkipNode* createNode(int key, int value, int level) {
    SkipNode *node = (SkipNode*)malloc(sizeof(SkipNode));
    node->key = key;
    node->value = value;
    node->forward = (SkipNode**)malloc((level + 1) * sizeof(SkipNode*));
    for (int i = 0; i <= level; i++) {
        node->forward[i] = NULL;
    }
    return node;
}

SkipList* createSkipList() {
    SkipList *list = (SkipList*)malloc(sizeof(SkipList));
    list->level = 0;
    list->size = 0;
    list->header = createNode(0, 0, MAX_LEVEL);
    return list;
}

int search(SkipList *list, int key, int *value) {
    SkipNode *cur = list->header;
    for (int i = list->level; i >= 0; i--) {
        while (cur->forward[i] != NULL && cur->forward[i]->key < key) {
            cur = cur->forward[i];
        }
    }
    cur = cur->forward[0];
    if (cur != NULL && cur->key == key) {
        *value = cur->value;
        return 1;
    }
    return 0;
}

void insert(SkipList *list, int key, int value) {
    SkipNode *update[MAX_LEVEL + 1];
    SkipNode *cur = list->header;
    
    for (int i = list->level; i >= 0; i--) {
        while (cur->forward[i] != NULL && cur->forward[i]->key < key) {
            cur = cur->forward[i];
        }
        update[i] = cur;
    }
    
    cur = cur->forward[0];
    if (cur != NULL && cur->key == key) {
        cur->value = value;
        return;
    }
    
    int newLevel = randomLevel();
    if (newLevel > list->level) {
        for (int i = list->level + 1; i <= newLevel; i++) {
            update[i] = list->header;
        }
        list->level = newLevel;
    }
    
    SkipNode *newNode = createNode(key, value, newLevel);
    for (int i = 0; i <= newLevel; i++) {
        newNode->forward[i] = update[i]->forward[i];
        update[i]->forward[i] = newNode;
    }
    list->size++;
}

int delete(SkipList *list, int key, int *value) {
    SkipNode *update[MAX_LEVEL + 1];
    SkipNode *cur = list->header;
    
    for (int i = list->level; i >= 0; i--) {
        while (cur->forward[i] != NULL && cur->forward[i]->key < key) {
            cur = cur->forward[i];
        }
        update[i] = cur;
    }
    
    cur = cur->forward[0];
    if (cur == NULL || cur->key != key) {
        return 0;
    }
    
    *value = cur->value;
    
    for (int i = 0; i <= list->level; i++) {
        if (update[i]->forward[i] != cur) break;
        update[i]->forward[i] = cur->forward[i];
    }
    
    while (list->level > 0 && list->header->forward[list->level] == NULL) {
        list->level--;
    }
    
    free(cur->forward);
    free(cur);
    list->size--;
    return 1;
}

void printSkipList(SkipList *list) {
    printf("跳跃表(共%d个节点,当前最高层=%d):\n", list->size, list->level);
    for (int i = list->level; i >= 0; i--) {
        printf("第%d层: ", i);
        SkipNode *cur = list->header->forward[i];
        while (cur != NULL) {
            printf("%d ", cur->key);
            cur = cur->forward[i];
        }
        printf("\n");
    }
}

void destroySkipList(SkipList *list) {
    SkipNode *cur = list->header->forward[0];
    while (cur != NULL) {
        SkipNode *temp = cur;
        cur = cur->forward[0];
        free(temp->forward);
        free(temp);
    }
    free(list->header->forward);
    free(list->header);
    free(list);
}

int main() {
    srand(time(NULL));
    
    SkipList *list = createSkipList();
    
    printf("=== 插入测试 ===\n");
    int keys[] = {3, 6, 7, 9, 12, 17, 19, 21, 25, 26};
    for (int i = 0; i < 10; i++) {
        insert(list, keys[i], keys[i] * 10);
        printf("插入 %d\n", keys[i]);
    }
    
    printSkipList(list);
    
    printf("\n=== 查找测试 ===\n");
    int val;
    if (search(list, 17, &val)) {
        printf("找到 17: value=%d\n", val);
    } else {
        printf("未找到 17\n");
    }
    
    if (search(list, 100, &val)) {
        printf("找到 100: value=%d\n", val);
    } else {
        printf("未找到 100\n");
    }
    
    printf("\n=== 删除测试 ===\n");
    if (delete(list, 7, &val)) {
        printf("删除 7 (value=%d)\n", val);
    }
    if (delete(list, 19, &val)) {
        printf("删除 19 (value=%d)\n", val);
    }
    
    printSkipList(list);
    
    destroySkipList(list);
    return 0;
}

运行结果:

text

复制代码
=== 插入测试 ===
插入 3
插入 6
...
跳跃表(共10个节点,当前最高层=4):
第4层: 12 
第3层: 3 12 25 
第2层: 3 6 12 17 25 
第1层: 3 6 7 9 12 17 19 21 25 
第0层: 3 6 7 9 12 17 19 21 25 26 

=== 查找测试 ===
找到 17: value=170
未找到 100

=== 删除测试 ===
删除 7 (value=70)
删除 19 (value=190)
跳跃表(共8个节点,当前最高层=4):
第4层: 12 
第3层: 3 12 25 
第2层: 3 6 12 17 25 
第1层: 3 6 9 12 17 21 25 
第0层: 3 6 9 12 17 21 25 26 

五、复杂度分析

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

最坏情况概率极低(随机层高保证)


六、跳跃表 vs 平衡树 vs 哈希表

对比项 跳跃表 平衡树(AVL/红黑树) 哈希表
实现复杂度 简单 复杂 中等
平均查找 O(log n) O(log n) O(1)
有序遍历 支持 支持 不支持
范围查询 支持 支持 不支持
空间开销 较大 中等 中等
并发友好 较好 一般

七、Redis ZSet 为什么用跳跃表

Redis 有序集合(ZSet)在元素较多时使用跳跃表 + 哈希表的组合:

  • 哈希表:提供 O(1) 的单元素查找

  • 跳跃表:提供有序遍历和范围查询

选择跳跃表的原因

  1. 实现比红黑树简单

  2. 平均 O(log n) 性能足够好

  3. 范围查询(ZRANGE)非常高效

  4. 代码量少,bug 更少


八、小结

这一篇我们实现了跳跃表:

操作 核心算法
查找 逐层下降,每层跳过小于 key 的节点
插入 查找每层前驱 + 随机层高 + 插入
删除 查找每层前驱 + 各层跳过节点

关键点

  • 随机层高保证平均性能

  • 最高层数影响空间和速度的平衡

  • Redis ZSet 底层就是这个结构

下一篇我们讲算法思想:递归与分治。


九、思考题

  1. 为什么跳跃表的层高要用随机生成,而不是固定值?

  2. 如果 P 值增大(如 0.75),对跳跃表的性能有什么影响?

  3. 如何实现跳跃表的范围查询(如查询所有 key 在 a, b 之间的节点)?

  4. 跳跃表在并发环境下比红黑树有什么优势?

欢迎在评论区讨论你的答案。

相关推荐
数模竞赛Paid answer5 小时前
2026年中青杯数学建模A题数学建模论文智能评估系统与多智能体优化方法求解全过程论文及程序
算法·数学建模·中青杯
银-豆豆6 小时前
数据结构与算法-动态规划、回溯与贪心
算法·动态规划
想吃火锅10057 小时前
【leetcode】200. 岛屿数量
算法·leetcode·职场和发展
hold?fish:palm8 小时前
24 回文链表
数据结构·c++·链表
Nil2088 小时前
leetcode 54螺旋矩阵
算法·leetcode·矩阵
依然鸣9 小时前
PTA团体程序设计天梯赛L1真题讲解L1-077-080
开发语言·c++·算法·深度优先·pat考试·图论
库玛西11 小时前
现代 C++ 智能指针全景指南:从 RAII 思想到工业级实践
c语言·开发语言·c++·笔记·面试
AC赳赳老秦11 小时前
风控岗应用:OpenClaw 采集公开司法与经营异常数据,自动生成企业风险评估报告
大数据·c语言·数据库·人工智能·python·php·openclaw
caimouse11 小时前
ReactOS 图形系统分析(19):颜色转换对象 — XLATEOBJ(xlateobj.c/h)
c语言·开发语言
qeen8712 小时前
【数据结构】自平衡二叉搜索树各种旋转算法原理解析及AVL树的C++实现
数据结构·c++·算法