简介

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。在大部分情况下,跳跃表的效率可以和平衡树相媲美,而且实现比平衡树更加简单。
Redis 使用跳跃表作为有序集合的底层实现之一,如果一个有序集合包含的元素较多,或者有序集合中的元素是较长的字符串时,Redis 就会使用跳跃表来维护数据。
接下来一起看下跳跃表的原理和基于C++的实现代码。

基本原理

跳跃表本质上是个有序的链表,其通过在节点上随机的添加辅助连接使得查找的时间复杂度从O(N)变为平均O(logN),最坏O(N)。

如上图所示,每个节点都有添加了辅助连接。我们可以通过辅助节点来加快搜索过程:在顶层的链表进行扫描,直到 遇到一个含有较小关键字且指向一个含有较大关键字节点的节点,或者到达这一层的最后一个节点,然后下降到下一层辅助节点继续查找,直到确认目标值不存在或者找到了目标值所在节点。举个例子,在上图中跳跃表寻找 58 的过程如下:

  • 初始时,位于头指针的第三层辅助节点
  • 通过当前辅助节点的next指针发现下一个节点的值为 12。
    因为12比目标值小,所以直接移动到 12 的第三层辅助节点
  • 因为 12 是最后一个第三层辅助节点,所以下降到第二层
  • 通过当前节点的next指针发现下一个节点为 78。
    因为 78 比目标值大,所以继续下降到第一层辅助节点。
  • 通过当前节点的next指针发现下一个节点为 56。
    因为 56 比目标值小,所以向后移动到 56 的第一层辅助节点。
  • 因为当前节点的数据比目标值小,且下一个节点的值(78)比目标值大,且此时已位于最低一层辅助节点,所以判定目标值不存在于该跳跃表中。查找结束。

Q:查找过程中为何没有用到数据节点自身的next指针呢?
A: 因为第一层的辅助节点等价于数据节点自身的next。其实在跳跃表中,数据节点只有数据,辅助节点中只有next指针。

Q: 当数据节点变多时,三层辅助节点好像不太够用?
A: 当跳跃表的数据节点变多时,我们可以加入更多层的辅助节点来保证足够快的查找速度。

跳跃表的初始化

为了初始化跳跃表,我们需要一个头结点,它含有M层辅助节点,每个辅助节点都指向NULL。M是整个跳跃表中辅助节点的层数上限。SkipList 类的定义如下,我们在构造函数中对头结点进行初始化。

// SkipList 的定义
template<typename DataType>
class SkipList {enum {MAX_LEVEL = 32, // 允许的最大层数};struct Node {std::vector<Node*> next; //存储辅助信息的 nextDataType data; // 数据};private:uint8_t max_level; //真正指定的最大层数Node head_node; //头结点int randomLevelNumber() const; //以概率 1/2^j 返回 j, j ∈ [1, max_level];public:SkipList(uint8_t ml = MAX_LEVEL) : max_level(ml) {if(max_level > MAX_LEVEL || max_level <= 2) {max_level = MAX_LEVEL;}head_node.next.resize(max_level); // 初始化头结点的 next 数组,全部指向 nullptr;}bool isExist(const DataType &) const; // 判断目标元素是否存在bool  erase(const DataType &); // 删除元素bool insert(const DataType &); // 插入元素typedef function<bool(const DataType&)> HandlerType;void walk(HandlerType &) const; // 暴露一个遍历接口
};

插入

当我们向跳跃表插入一个新节点时,需要解决的第一个问题就是新增节点要有多少层next指针。如果每 t 个节点中就有一个节点至少具备两层 next 指针,则我们在第二层可以一次跳跃 t 个节点,以此类推,每 t^j 个节点中,就有一个节点至少具备j+1层next指针。
为使节点具有上述性质,我们需要一个以概率 1/t^j 返回 j+1 的 随机函数。

// 随机函数的定义
// FIXME
// 这里有点问题,i 为 max_level 和 max_level-1 的概率是相同的,
template<typename DataType>
int SkipList<DataType>::randomLevelNumber() const {int i, j, t = rand();for(i = 1, j = 2; i < max_level; i++, j *= 2) {if(t > RAND_MAX/j) {break;}}return i;
}// 插入函数
template<typename DataType>
bool SkipList<DataType>::insert(const DataType &data) {if(isExist(data)) {return false;}int level = randomLevelNumber();Node *new_node = new Node();new_node->data = data;new_node->next.resize(level);int cur_level = max_level - 1; // 因为 level 是从 0 开始的。Node *cur_node = &head_node;while(cur_level >= 0) {while(cur_node->next[cur_level] != nullptr&& cur_node->next[cur_level]->data < data) {cur_node = cur_node->next[cur_level];}if(new_node->next.size() > cur_level) {new_node->next[cur_level] = cur_node->next[cur_level];cur_node->next[cur_level] = new_node;}-- cur_level;}return true;
}

调用了 1666112 次,得到的层数基本还是符合预期的,具体的分布如下:

层数 数量 比率
23 1 6.002e-07
22 1 6.002e-07
20 1 6.002e-07
19 3 1.8006e-06
18 7 4.2014e-06
17 15 9.003e-06
16 38 2.28076e-05
15 66 3.96132e-05
14 105 6.3021e-05
13 214 0.000128443
12 367 0.000220273
11 813 0.000487962
10 1598 0.000959119
9 3265 0.00195965
8 6385 0.00383228
7 13078 0.00784941
6 25849 0.0155146
5 52201 0.031331
4 103711 0.0622473
3 207830 0.12474
2 417042 0.250309
1 833522 0.50028

这个随机函数保证:

  • 平均每 1 个节点中就有一个至少具备 1 层辅助连接的节点。
  • 平均每 2 个节点中就有一个至少具备 2 层辅助连接的节点。
  • 平均每 4 个节点中就有一个至少具备 3 层辅助连接的节点。
  • 平均每 8 个节点中就有一个至少具备 4 层辅助连接的节点。
  • 以此类推。。。
  • 平均每 2^i 个节点中就有个一个至少具备 i-1 层辅助连接的节点。
插入的步骤

插入的步骤和搜索的套路类似,只是需要在插入过程中更新对应层的 next 指针,具体的流程如下:

  1. 首先判断待插入数据 x 是否已经存在于跳表中,如果存在则插入失败。
  2. 其次和链表的插入类似,需要先 new 一个结点 q 用于存储数据。
  3. 插入开始前,设指针 p 位于头结点的最高层,设当前层数为 cl。
  4. 移动 p,直到 p->next[cl] 为空或者 p->data 小于 p->next[cl]->data。
  5. 如果q在当前有指针,那么更新指针:
    1. q->next[cl] = p->next[cl]->next[cl]
    2. p->next[cl] = q
  6. 如果此时已位于最后一层,则插入结束。否则下降一层,即 cl -= 1,然后跳转步骤 4

以插入 9 为例,过程如下图所示:



删除

删除和插入过程类似,只是从建立链接变成了删除链接,寻找目标节点的流程是一样的,就不再赘述了。

template<typename DataType>
bool SkipList<DataType>::erase(const DataType &data) {int cur_level = max_level - 1;Node *cur_node = &head_node;while(cur_level >= 0) {while(cur_node->next[cur_level] != nullptr&& cur_node->next[cur_level]->data < data) {cur_node = cur_node->next[cur_level];}if(cur_node->next[cur_level] != nullptr&& !(data < cur_node->next[cur_level]->data)) {auto remove_node = cur_node->next[cur_level];cur_node->next[cur_level] = cur_node->next[cur_level]->next[cur_level];remove_node->next[cur_level] = nullptr;if(cur_level == 0) {delete(remove_node);}}--cur_level;}return 0;
}

总结

  • 相比单向链表,查找的时间复杂度从O(n) 降为 O(logN)。
  • 相比数组,删除插入的时间复杂度从O(n) 降为 O(logN),且无需扩容/缩容操作。
  • 相比平衡树/红黑树,各种操作的效率差不多,但是跳表不稳定。而且跳表的空间开销相比于后者有所增加。
  • 另外,可存储重复键值的跳表该如何实现呢?每个节点变为链表以解决冲突?

全部代码

#include <iostream>
#include <vector>
#include <stdlib.h>
#include <set>using namespace std;template<typename DataType>
class SkipList {enum {MAX_LEVEL = 32, // 允许的最大层数};struct Node {std::vector<Node*> next; //存储辅助信息的 nextDataType data; // 数据};private:uint8_t max_level; //真正指定的最大层数Node head_node; //头结点int randomLevelNumber() const; //以概率 1/2^j 返回 j, j ∈ [1, max_level];public:SkipList(uint8_t ml = MAX_LEVEL) : max_level(ml) {if(max_level > MAX_LEVEL || max_level <= 2) {max_level = MAX_LEVEL;}head_node.next.resize(max_level); // 初始化头结点的 next 数组,全部指向 nullptr;}bool isExist(const DataType &) const; // 判断目标元素是否存在bool  erase(const DataType &); // 删除元素bool insert(const DataType &); // 插入元素typedef function<bool(const DataType&)> HandlerType;void walk(HandlerType &) const; // 暴露一个遍历接口
};template<typename DataType>
void SkipList<DataType>::walk(HandlerType &handler) const {auto cur_node = &head_node;while(cur_node->next[0] != nullptr) {cur_node = cur_node->next[0];if(!handler(cur_node->data)) {break;}}
}// FIXME
// 这里有点问题,i 为 max_level 和 max_level-1 的概率是相同的,
template<typename DataType>
int SkipList<DataType>::randomLevelNumber() const {int i, j, t = rand();for(i = 1, j = 2; i < max_level; i++, j *= 2) {if(t > RAND_MAX/j) {break;}}return i;
}template<typename DataType>
bool SkipList<DataType>::isExist(const DataType &data) const {int cur_level = max_level - 1;const Node *cur_node = &head_node;while(cur_level >= 0) {while(cur_node->next[cur_level] != nullptr&& cur_node->next[cur_level]->data < data) {cur_node = cur_node->next[cur_level];}if(cur_node->next[cur_level] != nullptr&& !(data < cur_node->next[cur_level]->data)) {return true;}--cur_level;}return false;
}template<typename DataType>
bool SkipList<DataType>::erase(const DataType &data) {int cur_level = max_level - 1;Node *cur_node = &head_node;while(cur_level >= 0) {while(cur_node->next[cur_level] != nullptr&& cur_node->next[cur_level]->data < data) {cur_node = cur_node->next[cur_level];}if(cur_node->next[cur_level] != nullptr&& !(data < cur_node->next[cur_level]->data)) {auto remove_node = cur_node->next[cur_level];cur_node->next[cur_level] = cur_node->next[cur_level]->next[cur_level];remove_node->next[cur_level] = nullptr;if(cur_level == 0) {delete(remove_node);}}--cur_level;}return 0;
}template<typename DataType>
bool SkipList<DataType>::insert(const DataType &data) {if(isExist(data)) {return false;}int level = randomLevelNumber();Node *new_node = new Node();new_node->data = data;new_node->next.resize(level);int cur_level = max_level - 1; // 因为 level 是从 0 开始的。Node *cur_node = &head_node;while(cur_level >= 0) {while(cur_node->next[cur_level] != nullptr&& cur_node->next[cur_level]->data < data) {cur_node = cur_node->next[cur_level];}if(new_node->next.size() > cur_level) {new_node->next[cur_level] = cur_node->next[cur_level];cur_node->next[cur_level] = new_node;}-- cur_level;}return true;
}

Redis面试题系列:跳跃表相关推荐

  1. 学习笔记-Redis设计与实现-跳跃表

    跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的. 跳跃表支持平均O(logN).最坏O(N)复杂度的节点查找,还可以通过顺序性操 ...

  2. redis底层数据结构之跳跃表

    redis底层数据结构之跳跃表 redis 的zset有序连表为啥选择用跳跃表? 我们要思考一问题,首先多问问自己为什么,才容易理解它,ps:这是个人观点.首先我们选择的数据结构和算法原因有以下几种: ...

  3. mongodb 存储过程 遍历表数据_三、redis数据存储之跳跃表(SKIP LIST)

    导读 前面文章[一.深入理解redis之需要掌握的知识点 ]中,我们对redis需要学习的内容框架进行了一个梳理.[二.redis中String和List两种数据类型和应用场景 ].[二.redis中 ...

  4. 三、redis数据存储之跳跃表(SKIP LIST)

    导读 前面文章[一.深入理解redis之需要掌握的知识点 ]中,我们对redis需要学习的内容框架进行了一个梳理.[二.redis中String和List两种数据类型和应用场景 ].[二.redis中 ...

  5. Redis底层原理之跳跃表

    1. 什么是跳跃表? 增加了向前指针的链表叫作跳表.跳表全称叫做跳跃表.跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表.跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查 ...

  6. 4.redis设计与实现--跳跃表

    1.跳跃表由两个结构体构成: 2.总结: 转载于:https://www.cnblogs.com/jiangjing/p/8688471.html

  7. redis 系列7 数据结构之跳跃表

    redis 系列7 数据结构之跳跃表 原文:redis 系列7 数据结构之跳跃表 一.概述 跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问 ...

  8. Redis 为什么这么快? Redis 的有序集合 zset 的底层实现原理是什么? —— 跳跃表 skiplist

    Redis有序集合 zset 的底层实现--跳跃表skiplist Redis简介 Redis是一个开源的内存中的数据结构存储系统,它可以用作:数据库.缓存和消息中间件. 它支持多种类型的数据结构,如 ...

  9. Redis之跳跃表(面试重点容易考)

    跳跃表 1. 跳跃表的用处 2. 跳跃表的具体示例 跳跃表的查找 跳跃表的具体实现 本文重点 1. 跳跃表的用处 有序集合(zset)的底层可以采用数组, 链表, 平衡树等结果来实现, 但是他们都有各 ...

最新文章

  1. spring boot 学习(二)spring boot 框架整合 thymeleaf
  2. Silverlight从客户端上传文件到服务器
  3. Vbox配置仅主机模式
  4. 树莓派开始玩转linux pdf_用树莓派构建 Kubernetes 集群 | Linux 中国
  5. acess() 判断目录是否存在
  6. 流媒体技术的应用与发展前景
  7. 纯电动SUV哪吒U Pro即将上线:最高续航610公里
  8. stl的multiset和set和priority_queue区别
  9. JavaWeb那些事儿(二)--java中类、成员和方法的访问权限
  10. 科研 | 如何找到研究的突破点?
  11. SpringBoot 启动过程,你不知道的秘密!
  12. nmake编译dll
  13. 谱瑞PS8625替代方案|PS8622替代方案|高性价比EDP转LVDS转接板方案CS5211设计开发
  14. 流程图制作原则与示例
  15. 非常详细的范式讲解(1NF/2NF/3NF/BCNF)
  16. 计算机鼠标不灵活怎么办,鼠标不灵怎么办 鼠标不灵的常见解决方法
  17. 【Linux 4】定时任务调度与进程服务管理
  18. python星星闪烁_python实现while循环打印星星的四种形状
  19. 网站盈利模式分析总结
  20. RMF模型评分制计算方法(2021/08/04)

热门文章

  1. 云端软件平台 自己封装软件 图标不正常怎么办
  2. matlab 高阶拟合,matlab – 将多项式拟合到函数的最大值
  3. 布兰特原油飙升至多年高位
  4. python中any()函数用法详解
  5. java如何获得组合框并运用,Swing如何使用组合框?
  6. 三年投 1000 亿,达摩院何以仗剑走天涯?
  7. keil编译时,提示function “xxx“ declared implicitly错误解决办法
  8. 【华为OD机试真题 C++语言】35、解密犯罪时间 | 机试真题+思路参考+代码解析
  9. 手机端车牌号码键盘的vue组件
  10. 考研机试:数学问题之日期类问题