概念
Redis中又一个重要的数据结构——跳跃表。跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。大部分情况下,跳跃表的效率可以和平衡二叉树相媲美,并且跳跃表比平衡二叉树实现更简单,操作也更容易。Redis使用跳跃表作为有序集合的底层实现之一。举个例子:fruit-price是一个有序集合键,水果名为成员,价格为分值,保存了130款水果的价钱
redis-> ZRANGE fruit-price 0 2 WITHSCORES
1) "banana"
2) "5"
3) "cherry"
4) "6.5"
5) "apple"
6) "8"
redis-> ZCARD fruit-price
(integer) 130
跳跃表有redis.h/zskiplistNode 和 reids.h/zskiplist 两个结构定义,一个表示节点,一个存储跳跃表相关信息,稍后我们来看具体定义,先看一个结构图。
图片最左边是zskiplist结构,该结构包含以下属性:
- header 指向跳跃表的表头节点
- tail 指向跳跃表的表尾节点
- level 记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)
- length 记录跳跃表的长度,也就是跳跃表目前包含的节点的数量
右方的是四个zskiplistNode结构,包含以下属性:
- 层(level)节点中用L1,L2,L3等字样标记节点的各个层,L1代表第一层,L2代表第二层,以此类推。每个层都有两个属性,前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离。在上面的图中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾遍历时,访问会沿着层的前进指针进行。
- 后退(backward)指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
- 分值(score):各个节点中1.0,2.0和3.0是节点所保存的分值,在跳跃表中,节点按各自所保存的分值从小到大排列。
- 成员对象(obj):各个节点中的o1,o2和o3是节点所保存的成员对象
表头节点和其他节点是一样的,也有后退指针,分值和成员对象,不过表头节点的这些属性都不会用到,所以图中省略了这些部分,只显示了表头节点的各个层。
节点实现
typedef struct zskiplistNode{
struct zskiplistLevel {
struct zskiplistNode * forward;
unsigned int span;
} level[];
struct zskiplistNode * backward;
double score;
obj * obj;
} zskiplistNode;
- 层:跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他几点的速度就越快。每次创建一个跳跃表节点的时候,程序都根据幂次定律(power law,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是这个层的高度。
- 前进指针:用于遍历跳跃表,举个例子:
- 迭代程序首先访问跳跃表的第一个节点(表头),然后从第四层的前进指针移动到表中的第二个节点。
- 在第二个节点,程序沿着第二层的前进指针移动到表中的第三个节点
- 在第三个节点,程序同样沿着第二层的前进指针移动到表中的第四个节点
- 当程序再次沿着第四个节点的前进指针移动时,它碰到一个null,程序知道这是已经到达了跳跃表的表尾,于是结束这次遍历
- 跨度:层的跨度用与记录两个节点之间的距离,看上去span好像与遍历有关,但是前面的遍历过程并没有用到这个属性,其实span是用来rank的,比如需要快速知道某个节点在跳跃表中排序,可以使用span计算得出。
- 后退指针:每个节点只有一个后退指针,后退指针在跳跃表中用于后退操作,与前进指针不同,每次只能后退一个节点。
- 分值和成员:分值时double类型,跳跃表按分值排序,成员时obj类型,指针指向SDS字符串。
跳跃表定义
typedef struct zskiplist {
struct zskiplistNode * header, *tail;
unsigned long length;
int level;
} zskiplist;
看了定义,应该就知道这个是保存跳跃表相关信息的,header,tail指针用于快速操作跳跃表,长度也是记录节点的数量。level属性用于获取表中层最大的那个节点的层数量,注意表头的高度不算在内。这个属性在查询跳跃表上很重要,稍后介绍。
查询操作
前面说到,跳跃表查询效率平均为O(logN)最坏O(N)的时间复杂度,最好是什么情况最坏又是什么情况?
如果把跳跃表理解为平衡二叉树就很好理解复杂度的问题。把跳跃表的最高层想象为平衡二叉树的根节点,每次查询是从最高层出发,这个时候就用到了前面说的level属性,找到头节点相应的层,并移动到最高层的节点,判断其值是否时查询值,如果不是则根据大小关系,在平衡二叉树中大于目标值则向左,小于目标值向右子节点移动,跳跃表中相前相后移动,最终找到查询址,这样的话复杂度就和平衡二叉树一致,是O(logN)了,最坏情况就是最高节点是表尾节点,查询的是最小值,则复杂度为O(N),这样就很好理解了。