【数据结构与算法】第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. 跳跃表在并发环境下比红黑树有什么优势?

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

相关推荐
凯瑟琳.奥古斯特22 分钟前
力扣1008:前序重建BST
开发语言·c++·算法·leetcode·职场和发展
aqiu1111111 小时前
【算法日记 13】LeetCode 49. 字母异位词分组:哈希表的进阶“降维打击”
算法·leetcode·散列表
WL学习笔记1 小时前
链式队列(数据结构)
前端·数据结构·jquery
KaMeidebaby1 小时前
卡梅德生物技术快报|纳米抗体技术全套实操流程:AFB1 全合成文库淘选 + 分子对接定点突变参数手册
人工智能·python·tcp/ip·算法·机器学习
巴糖2 小时前
从做早餐理解 DAG 与拓扑排序:任务依赖图的核心逻辑
算法
2601_962851742 小时前
计算机毕业设计之基于YOLOV10的低光照目标检测算法研究与实现
大数据·算法·yolo·目标检测·信息可视化·cnn·课程设计
Darkwanderor2 小时前
Linux进程优先级操作
linux·运维·c语言·c++
magicwt2 小时前
快手自动出价论文GAVE阅读笔记
算法
智者知已应修善业2 小时前
【P12159蓝桥杯数组翻转】2025-4-24
c语言·c++·经验分享·笔记·算法·蓝桥杯
SuperByteMaster2 小时前
keil link misc controls
c语言