跳跃表(skiplist)是一种有序数据结构,它通过在每一个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
跳跃表支持平均时间复杂度为O(logN)、最坏O(N)的节点查找,还可以通过顺序性操作来批量处理节点(大部分情况,跳跃表的效率可以和平衡树相媲美,却更为简单)。
Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员(member)是比较长的字符串时,Redis就会使用跳跃表来作为有序集合的底层实现。
本篇主要针对跳跃表的实现及数据结构、属性进行总结介绍。
跳跃表
整个跳跃表的结构如下图(图1):
备注:图1中连线上带有数字的箭头表示前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历是,访问会沿着层的前进指针进行。
其属性如下:
- header:指向跳跃表的表头节点
- tail:指向跳跃表的表尾节点
- level:记录跳跃表内,层数最大的节点的层数(表头节点层数不计算在内)。
- length:记录跳跃表的长度,故获取跳跃表的长度时间复杂度为O(1).
跳跃表节点
位于zskiplist结构右方的是四个zskiplistNode结构,而每个跳跃表节点zskiplistNode结构,其结构如下图(图2):
其代码实现如下:
typedef struct zskiplistNode{
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
解析:
-
层:level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度。层数越多,访问其他节点的速度越快。
每创建新的zskiplistNode时,程序使用幂次定律(power law, 越大的数出现的概率越小),随机生成一个介于1-32的值作为level数组的大小,即
高度。 -
跨度:用于记录两个节点的距离。遍历操作只使用前进指针即可,跨度实际上是用来计算排位的:在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表的排位。
-
后退指针:用于从表尾向表头方向访问节点:跟可以一次跳过多个节点的前进指针不同,因为每个节点只有一个后退指针,所以每次只能后退至前一个节点。
-
分值和成员:分值是一个浮点数,跳跃表中所有节点都按分值从小到大排序。成员指向一个SDS对象。
同时,各节点之间成员必须是唯一的,但是各节点保存的分值可以是相同的。排序:
分值--成员大小, 小的在前,大的在后面。
查找方式
比如输入命令 -- ZADD page_rank 10 google.com:
- 通过上述方式生成--zskiplistNode
- 根据score查找需要插入的位置,通过遍历zskiplistLevel找到小于10的最大跨度level,比如找到level4找到节点o4,分值为8,那么还需要遍历节点o4的zskiplistLevel找到 <=2的最大跨度level,以此类推,找到指定的位置,让新增节点level的其中层指向下一个节点,前面节点指向新的节点,其时间复杂度为 O(log(N))。
如果从表尾向表头遍历跳跃表中的所有节点:
程序先通过跳跃表的tail指针访问表尾节点,然后通过后退指针访问倒数第二个节点,之后再沿着后退指针访问倒数第三个节点,再后遇到指向NULLde后退指针,访问结束。
over~~~~~~~~~~~~~