【Redis】数据结构与对象(六种底层数据结构)——跳跃表 skiplist
跳跃表 skiplist
有序链表只能逐一查找元素,操作起来比较慢,则出现了跳表,跳表就是在链表的基础上,增加了多级索引,通过索引位置的几个跳转,它通过在每个节点维持多个指向其他节点的指针,而达到快速访问节点的目的,实现数据的快速定位。
**skiplist的作用:**一是实现有序集合键,二是在集群节点中用作内部数据结构
跳表的数据结构:
(1)跳跃表的定义
跳跃表其实可以把它理解为多层的链表,它有如下的性质
- 多层的结构组成,每层是一个有序的链表
- 最底层(level 1)的链表包含所有的元素
- 跳跃表的查找次数近似于层数,时间复杂度为O(logn),插入、删除也为 O(logn)
- 跳跃表是一种随机化的数据结构(通过抛硬币来决定层数)
那么如何来理解跳跃表呢,我们从最底层的包含所有元素的链表开始,给定如下的链表
然后我们每隔一个元素,把它放到上一层的链表当中,这里我把它叫做上浮(注意,科学的办法是抛硬币的方式,来决定元素是否上浮到上一层链表,我这里先简单每隔一个元素上浮到上一层链表,便于理解),操作完成之后的结构如下
查找元素的方法是这样,从上层开始查找,大数向右找到头,小数向左找到头,例如我要查找17
,查询的顺序是:13 -> 46 -> 22 -> 17;如果是查找35
,则是 13 -> 46 -> 22 -> 46 -> 35;如果是54
,则是 13 -> 46 -> 54
上面是查找元素,如果是添加元素,是通过抛硬币的方式来决定该元素会出现到多少层,也就是说它会有 1/2的概率出现第二层、1/4 的概率出现在第三层…
跳跃表节点的删除和添加都是不可预测的,很难保证跳表的索引是始终均匀的,抛硬币的方式可以让大体上是趋于均匀的
假设我们已经有了上述例子的一个跳跃表了,现在往里面添加一个元素18
,通过抛硬币的方式来决定它会出现的层数,是正面就继续,反面就停止,假如我抛了2次硬币,第一次为正面,第二次为反面
跳跃表的删除很简单,只要先找到要删除的节点,然后顺藤摸瓜删除每一层相同的节点就好了
跳跃表维持结构平衡的成本是比较低的,完全是依靠随机,相比二叉查找树,在多次插入删除后,需要Rebalance来重新调整结构平衡
(2)跳跃表的实现
Redis的跳跃表实现是由redis.h/zskiplistNode
和redis.h/zskiplist
两个结构定义,zskiplistNode
定义跳跃表的节点,zskiplist
保存跳跃表节点的相关信息
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
// 成员对象 (robj *obj;)
sds ele;
// 分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针,用于从表头向表尾方向访问节点,用于遍历操作
struct zskiplistNode *forward;
// 跨度——两个节点的跨度越大,相距越远
// 跨度实际上是用来计算元素排名(rank)的,在查找某个节点的过程中,将沿途访过的所有层的跨度累积起来,得到的结果就是目标节点在跳跃表中的排位
unsigned long span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;
zskiplistNode
结构
level
数组(层):每次创建一个新的跳表节点都会根据幂次定律计算出level数组的大小,也就是次层的高度,每一层带有两个属性-前进指针和跨度,前进指针用于访问表尾方向的其他指针;跨度用于记录当前节点与前进指针所指节点的距离(指向的为NULL,阔度为0)- 前进指针(level[i].forward):用于从表头向表尾方向访问节点
- 跨度(level[i].span):用于记录两个节点之间的距离。两个节点之间的跨度越大,它们相距得就越远。指向null的所有前进指针的跨度都为0,因为它们没有连向任何节点。
backward
(后退指针):指向当前节点的前一个节点score
(分值):用来排序,如果分值相同看成员变量在字典序大小排序obj
或ele
:成员对象是一个指针,指向一个字符串对象,里面保存着一个sds;在跳表中各个节点的成员对象必须唯一,分值可以相同
zskiplist
结构
header
、tail
表头节点和表尾节点length
表中节点的数量level
表中层数最大的节点的层数
zskiplist
的头结点不是一个有效的节点,它有ZSKIPLIST_MAXLEVEL层(32层),每层的forward
指向该层跳跃表的第一个节点,若没有则为NULL,在Redis中,上面的跳跃表结构如下
- 每个跳跃节点的层高都是1至32之间的随机数
- 在同一个跳跃表中,对个节点可以包含相同的分支,但每个节点的成员对象必须是唯一的
- 跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序