跳跃表在Redis中主要用于有序集合键的实现,其他地方没怎么用到,但是这种数据结构在面试的时候经常会问到,因为它作为一种查找时间复杂度为O(logN)的特殊的链表,效率堪比红黑树或平衡树,而实现难度却远小于它们。下面分3个模块讲解Redis的跳跃表实现:
一、跳跃表的应用场景
在Redis中,当有序集合包含的元素数量较多,或者有序集合中元素的成员是比较长的字符串时,就会使用跳跃表做有序集合键的底层实现。其中,跟有序集合有关的命令主要有ZADD、ZRANGE等
二、跳跃表的数据结构
首先介绍下跳跃表的节点定义:
type struct zskiplistNode {
struct zskiplistLevel {
struct zskiplistNode *forward; //前进指针
unsigned int span; //跨度
}level[]; //level数组,数组索引表示层数,最多32层(索引值为0~31)
struct zskiplistNode *backward; //后退指针
double score; //分数
robj *obj; //成员对象
}zskiplistNode;
看起来很复杂是吧,没关系,下面对结构体的所有成员变量一个个介绍:
1. level[]
每个跳跃表节点都包含一个level数组,这个数组包含1~32个元素,具体是多少呢,由幂次定律决定,假设为1的概率为,那么为2的概率就为
,以此类推,为n的概率为
,这个数组元素越多,说明层数越多,访问其他节点的速度就会越快。
level数组的每个元素都是一个zskiplistLevel结构体变量,这个变量包含2个元素forward和span,其中forward是一个指向下一个跳跃表节点的前进指针,span表示与下一个跳跃表节点的跨度,不理解的话可以看下面的例子说明。
2. backward
后退指针主要用于指向上一个跳跃表节点,形成逆向链表(个人觉得作用不大)
3. score
有序集合的分值,排序时用到,决定了跳跃表节点的位置
4. obj
obj是一个指向实际成员对象的指针
下面介绍完整的跳跃表定义:
typedef struct zskiplist {
struct zskiplistNode *header, *tail; //分别指向跳跃表头和表尾
unsigned long length; //跳跃表中节点数量
int level; //跳跃表中除跳跃表头节点之外层数最大的节点的层数,最大为32
}zskiplist;
其中需要注意的一点是,header指向的跳跃表头节点固定有32层(即该节点对应的level数组有32个元素),且一开始的时候每一层指向的下一个跳跃表节点都为NULL,直到有节点插入到跳跃表中去。
三、跳跃表的插入过程(重点):
1. 一开始跳跃表是空的,如下图:
2. 先插入元素A,对应的score为5,随机生成的层数为2
3. 再插入元素B,对应的score为3,随机生成的层数为1
4. 最后再插入元素C,对应的score为7,随机生成的层数为1
5. 此时如果要查找分值为7的元素C,先从最高层的链表往右查,发现最上面的链表的第一个元素(不考虑header指向的节点)的分值为5<7,就往右前进,此时最上面的链表已经没有下一个节点了,所以就降一层再往右查,进而查到元素C的存在,整个过程其实直接跳过了对元素A对应节点的访问,从而提升了查询效率。
总结:
其实跳跃表就是由level条前进链表+1条后退链表构成的,后退链表的存在主要用于从后往前遍历所有跳跃表节点,而多条前进链表的存在则是为了提升查询效率。