【数据结构与算法】第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 插入
用 update 数组记录每层插入位置的前驱节点
随机生成新节点的层高
如果层高超过当前最大层,更新 update 中超出部分为 header
在每层插入新节点
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 删除
用 update 数组记录每层要删除节点的前驱
如果找到要删除的节点,在各层将其跳过
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) 的单元素查找
跳跃表:提供有序遍历和范围查询
选择跳跃表的原因:
实现比红黑树简单
平均 O(log n) 性能足够好
范围查询(ZRANGE)非常高效
代码量少,bug 更少
八、小结
这一篇我们实现了跳跃表:
| 操作 | 核心算法 |
|---|---|
| 查找 | 逐层下降,每层跳过小于 key 的节点 |
| 插入 | 查找每层前驱 + 随机层高 + 插入 |
| 删除 | 查找每层前驱 + 各层跳过节点 |
关键点:
随机层高保证平均性能
最高层数影响空间和速度的平衡
Redis ZSet 底层就是这个结构
下一篇我们讲算法思想:递归与分治。
九、思考题
为什么跳跃表的层高要用随机生成,而不是固定值?
如果 P 值增大(如 0.75),对跳跃表的性能有什么影响?
如何实现跳跃表的范围查询(如查询所有 key 在 [a, b] 之间的节点)?
跳跃表在并发环境下比红黑树有什么优势?
欢迎在评论区讨论你的答案。
