一、定义
-
跳跃表是一种有序的数据结构,它通过在每个节点中维持多个指向其它节点的指针来达到快速访问节点的目的。
如下图:节点1有指向节点2、节点3、节点4的指针,这样如果要访问节点4的数据,节点1可以直接通过指向节点4的指针来快速查询节点4的数据,而不需要依次遍历所有的节点。
-
跳跃表的时间复杂度平均位OlogN,最坏为O(N)。在大多数情况下跳跃表的效率可以和平衡树相媲美,并且跳跃表的实现比平衡树更加简单。
下图中实现了两个数据结构,平衡树和跳跃表(跳跃表中的数据是顺序存储)
如果要查询4这个数据,那么在平衡树中走过7--->2--->4
,那么在跳跃表中走过节点1--->节点4,或者节点1--->节点2--->节点4
-
Redis使用跳跃表作为有序集合键的底层之一,如果一个有序集合包含元素数量比较多,或者有序集合中的元素是比较长的字符串,Redis就会使用跳跃表来作为有序集合键的底层实现。
-
Redis在两个地方使用了跳跃表:一、有序集合键;二、集群节点中用作内部数据结构。
二、实现
2.1、结构
Redis跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zskiplist结构则用于保存跳跃表节点的相关信息:节点数量、表头节点和表尾节点的指针等。
结构图如下:下图中有一个zskiplist和四个zskiplistNode
- zskiplist中包含四个属性分别为:
- header:指向跳跃表的表头节点
- tail:指向跳跃表的表尾节点
- level:记录跳跃表中层数最大的节点的层数
- length:记录跳跃表的长度(跳跃表中包含节点的数量去掉表头)
- zskiplistNode包含四个属性分别为:
- 层(level):每个节点中用L1、L2标记的块被是每个节点的层,每层有两个属性:前进指针和跨度
a. 前进指针:前进指针用于访问指向的其它节点
b. 跨度:记录当前指针指向的节点和当前节点的距离(例如节点1中指向节点4的跨度为3) - 后退(backward):每个节点中标出的BW就是节点的后退指针,BW指向当前节点的前一个节点
- 分值(score):节点中存储的值,例如节点1中存储的2就是节点1的分值
- 成员对象(obj):节点中的O1、O2、O3代表节点保存的成员对象
2.2、代码定义
typedef struct zskiplistNode{
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
} level[];
} zskiplistNode;
三、总结
- 跳跃表节点的level数组包含多个元素,每个元素都包含一个指向其它节点的指针,程序可以通过这些层来加快访问其它节点的速度,一般来说层的数量越多,访问其它节点的速度越快。
- 指向NULL的所有前进指针跨度为0,因为它们没有连向其它任何节点。
- 遍历操作只使用前进指针,跨度是用来计算排位的。
- 每个节点只有一个后退指针,每次只能后退到前一个节点,不能跨越后退。
- header和tail是跳跃表中的表头和表尾,所以表头和表尾的复杂度为O1. 跳跃表中的节点按照分值的大小进行排序的,分值相同时,节点会按照成员对象的大小进行排序(都是从小到大)。
- 跳跃表中多个节点可以保存相同的分值,但是每个节点的成员对象必须是唯一的。