当前位置: 首页 > news >正文

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

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

http://www.jsqmd.com/news/642386/

相关文章:

  • ITensors——一个聪明的张量网络库(3)
  • 从“AI仿生人”到“原创音乐人”:普通人如何用AI写歌、发歌、赚钱
  • 网页游戏市场每日分析|二级市场传奇页游平台排名|602游戏平台
  • JDK安装及JRE说明
  • fastapi2
  • Wazuh OVA镜像部署实战:从零搭建开源XDR-SIEM一体化平台
  • AI 到底会不会取代人类?从四大行业落地真相看程序员的“危”与“机”
  • SITS2026多模态搜索上线前48小时:一场召回率突降38%的故障溯源与反脆弱加固
  • 2026年排行好的找工作招工平台推荐 - 品牌宣传支持者
  • D3KeyHelper终极指南:5分钟掌握暗黑3技能自动化神器
  • STM32F103实战:Zbar库移植与二维码识别优化指南
  • FT232H连接Vivado出现问题2026
  • OpenVSP:快速上手指南!5分钟学会开源参数化飞机设计
  • 新手SRC挖掘实战 | 一次从信息泄露到校园教务后台的完整路径
  • 从CSS选择器到DOM树匹配:Easy-Scraper如何重构网页数据提取的技术范式
  • 光影的艺术:从入门到电影级宣传片的布光与器材全解析
  • CDLF多级泵在高层供水系统中稳不稳?关键不在参数,而在这4个点
  • 比特 GEO 优化:亳州本地AI 搜索排名与本地地理定位双引擎,药都企业精准获客首选
  • 别再手动算脉冲了!用STM32CubeMX的编码器模式,5分钟搞定直流电机测速(附防溢出处理代码)
  • 入行AI应用开发?AI应用开发岗都是先混进去再说!
  • AI创作利器:Harness+OpenClaw+CLI实战
  • 先免费试用下Claude code安装使用(教程)
  • web后端python安全-总结
  • 电动牙刷语音播报蓝牙屏驱电机驱动八大解决方案
  • 华为云引领工业软件云端革命,【aigc】chrome-devtools-mcp怎么玩?。
  • 从GTP到GTM:深入解析Xilinx Ultrascale系列GT收发器的演进与选型指南
  • 提升企业知识使用率的运营活动设计指南
  • INTERFACE AZI-2502接口输出模块
  • Mysql--基础知识点--98--临键锁 VS 间隙锁
  • 除螨仪到底有没有效果?2026 十款家用高性价比除螨仪品牌精选推荐