跳跃表介绍
跳跃表(skiplist)是一个有序数据结构,它通过每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
Redis使用跳跃表来作为有序集合键的底层实现,除了在有序集合键作为底层实现,跳跃表还作为集群节点中内部数据结构的底层实现,Redis只有在这两个地方使用到了跳跃表。
跳跃表的实现
Redis跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义,其中zskiplistNode作为跳跃表节点,zskiplist结构则用于保存跳跃表节点的相关信息。
跳跃表节点
跳跃表节点由redis.h/zskiplistNode结构定义:
typedef struct zskiplistNode{
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
} level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj
}zskiplistNode;
-
层
跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来快速访问其他节点。一般来说,层的数量越多,访问其他节点的速度就会越快。
每次创建一个新跳跃表节点的时候,程序根据幂次定律(越大的数出现的概率越小)随机生成一个介于1和32之间的数值作为level数组的大小,这个大小就是层的高度。 -
前进指针
每个层都有一个指向表尾方向的前进指针,用于从表头向表尾方向访问节点。下图用虚线表示出了程序从表头向表尾方向,遍历跳跃表中所有节点的路径:
当程序沿着前进指针遍历遇到null时,就表示已经访问到了跳跃表节点的表尾。 -
跨度
层的跨度用于记录两个节点之间的距离:- 两个节点之间的跨度值越大,表示两个节点相距越远
- 指向NULL的所有前进指针的跨度都为0,因为它们没有连向任何节点
跨度实际上是用来计算排位(rank)的:在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表中的排位。
-
后退指针
节点的后退指针用于从表尾向表头方向访问节点:跟可以一次跳过多个节点的前进指针不同,因为每个节点只有一个后退指针,所以每次只能后退至前一个节点。 -
分值和成员
节点的分值是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序。
节点的成员对象是一个指针,它指向一个字符串对象,而字符串对象则保存着一个SDS值。
在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是各个节点的分值可以是不唯一的:
分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象小的节点排在前面(表头方向),成员对象大的节点排在后面(表尾方向)。
如下图,分值相同,但是成员对象的字典序o1<o2<o3。
跳跃表
通过使用一个zskiplist结构来持有这些节点,程序可以更方便的对整个跳跃表进行处理,比如快速访问跳跃表表头节点和表尾节点,或者快速获取跳跃表的节点个数。
zskiplist结构定义如下:
typedef struct zskiplist{
// 表头节点和表尾节点
structz skiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
}zskiplist;