共计 2662 个字符,预计需要花费 7 分钟才能阅读完成。
跳跃表 (skiplist) 是一种有序数据结构, 它通过在每个节点中维持多个指向其余节点的指针, 从而达到快速访问节点的目标.
跳跃表反对均匀 O(log N) 最坏 O(N) 复杂度的节点查找, 还能够通过程序性操作来批量解决节点.
在大部分状况下, 跳跃表的效率能够和均衡树相媲美, 并且因为跳跃表的实现比均衡树要来得更为简略, 所以有不少程序都应用跳跃表来代替均衡树.
Redis 应用跳跃表作为有序汇合键的底层实现之一: 如果一个有序汇合蕴含的元素数量比拟多, 又或者有序汇合中元素的成员 (member) 是比拟长的字符串时, Redis 就会应用跳跃表来作为有序汇合键的底层实现.
和链表, 字典等数据结构被宽泛地利用在 Redis 外部不同, Redis 只在两个中央用到了跳跃表, 一个是实现有序汇合键, 另一个是在集群节点中用作外部数据结构, 除此之外, 跳跃表在 Redis 外面没有其余用处.
1. 跳跃表节点
跳跃表节点由 redis.h/zskiplistNode
构造定义:
typedef struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 后退指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];} zskiplistNode;
1.1 层
跳跃表节点的 level 数组能够蕴含多个元素, 每个元素都蕴含一个指向其余节点的指针, 程序能够通过这些层来放慢拜访其余节点的速度, 一般来说, 层的数量越多, 拜访其余节点的速度就越快.
每次创立一个新跳跃表节点的时候, 程序都依据幂次定律 (power law,越大的数呈现的概率越小) 随机生成一个介于 1 和 32 之间的值作为 level 数组的大小, 这个大小就是层的“高度”.
1.2 后退指针
每个层都有一个指向表尾方向的后退指针 (level[i].forward 属性), 用于从表头向表尾方向拜访节点.
1.3 跨度
层的跨度 (level[i].span 属性) 用于记录两个节点之间的间隔:
- 两个节点之间的跨度越大, 它们相距得就越远.
- 指向 NULL 的所有后退指针的跨度都为 0 , 因为它们没有连向任何节点.
跨度用来计算排位 (rank) : 在查找某个节点的过程中, 将沿途拜访过的所有层的跨度累计起来, 失去的后果就是指标节点在跳跃表中的排位.
1.4 后退指针
节点的后退指针 (backward) 用于从表尾向表头方向拜访节点, 它每次只能后退至前一个节点.
1.5 分值和成员
节点的分值 (score) 是一个 double 类型的浮点数, 跳跃表中的所有节点都按分值 从小到大
排序.
节点的成员对象 (obj) 是一个指针, 它指向一个 字符串对象
, 而字符串对象则保留着一个 SDS 值.
在同一个跳跃表中, 各个节点保留的 成员对象必须是惟一的
, 然而多个节点保留的 分值却能够是雷同的
: 分值雷同的节点将依照成员对象在字典序中的大小来进行排序, 成员对象较小的节点会排在后面(凑近表头的方向), 而成员对象较大的节点则会排在前面(凑近表尾的方向).
2. 跳跃表
应用一个 zskiplist
构造来持有节点, 能够更不便地拜访跳跃表的表头节点和表尾节点, 又或者疾速地获取跳跃表节点
的数量 (也即是跳跃表的长度) 等信息.
zskiplist
构造的定义如下 (redis.h/zskiplist
):
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;
header
和 tail
指针别离指向跳跃表的表头和表尾节点, 程序定位表头节点和表尾节点的复杂度为 O(1) .
length
属性记录节点的数量, 可在 O(1) 复杂度内返回跳跃表的长度.
level
则用于在 O(1) 复杂度内获取跳跃表中层高最大的那个节点的层数量, 留神表头节点的层高并不计算在内.
3. 跳跃表 API
函数 | 作用 | 工夫复杂度 |
---|---|---|
zslCreate | 创立一个新的跳跃表. | O(1) |
zslFree | 开释给定跳跃表,以及表中蕴含的所有节点. | O(N),N 为跳跃表的长度. |
zslInsert | 将蕴含给定成员和分值的新节点增加到跳跃表中. | 均匀 O(log N),最坏 O(N),N 为跳跃表长度. |
zslDelete | 删除跳跃表中蕴含给定成员和分值的节点. | 均匀 O(log N),最坏 O(N),N 为跳跃表长度. |
zslGetRank | 返回蕴含给定成员和分值的节点在跳跃表中的排位. | 均匀 O(log N),最坏 O(N),N 为跳跃表长度. |
zslGetElementByRank | 返回跳跃表在给定排位上的节点. | 均匀 O(log N),最坏 O(N),N 为跳跃表长度. |
zslIsInRange | 给定一个分值范畴(range), 比方 0 到 15,20 到 28,诸如此类, 如果给定的分值范畴蕴含在跳跃表的分值范畴之内, 那么返回 1,否则返回 0 . |
通过跳跃表的表头节点和表尾节点, 这个检测能够用 O(1) 复杂度实现. |
zslFirstInRange | 给定一个分值范畴, 返回跳跃表中第一个合乎这个范畴的节点. | 均匀 O(log N),最坏 O(N).N 为跳跃表长度. |
zslLastInRange | 给定一个分值范畴, 返回跳跃表中最初一个合乎这个范畴的节点. | 均匀 O(log N),最坏 O(N).N 为跳跃表长度. |
zslDeleteRangeByScore | 给定一个分值范畴, 删除跳跃表中所有在这个范畴之内的节点. | O(N),N 为被删除节点数量. |
zslDeleteRangeByRank | 给定一个排位范畴, 删除跳跃表中所有在这个范畴之内的节点. | O(N),N 为被删除节点数量. |
4. 总结
- 跳跃表是有序汇合的底层实现之一, 除此之外它在 Redis 中没有其余利用;
- Redis 的跳跃表实现由 zskiplist 和 zskiplistNode 两个构造组成, 其中 zskiplist 用于保留跳跃表信息(比方表头节点, 表尾节点, 长度), 而 zskiplistNode 则用于示意跳跃表节点;
- 每个跳跃表节点的层高都是 1 至 32 之间的随机数;
- 在同一个跳跃表中, 多个节点能够蕴含雷同的分值, 但每个节点的成员对象必须是惟一的;
- 跳跃表中的节点依照分值大小进行排序, 当分值雷同时, 节点依照成员对象的大小进行排序.
文章来源于自己博客,公布于 2018-05-16,原文链接:https://imlht.com/archives/132/