1.概述
跳跃表是一种有序的数据结构,通过其各个节点中维护有多个指向其他节点的指针来达成快速访问的目的。它在查找上具有复杂度平均O(logN)、最坏O(N)的优势,还能利用顺序性批量处理节点,使得在大多数情况下,跳跃表的效率与平衡树不分伯仲。
2.实现
跳跃表在Redis中由zskiplistNode和zskiplist两个结构定义,分别代表了跳跃表节点和保存跳跃表节点的信息。
如图1为一个简单的跳跃表示例,从左往右分为zskiplist结构和zskiplistNode结构两部分,下面将简述其中属性变量的含义:
- header:指向跳跃表的表头节点。
- tail:指向跳跃表的表尾节点。
- level:记录节点的最大层次。
- length:记录跳跃表长度。
- 层(level):此属性位于zskiplistNode,包含前进指针和跨度。前进指针指向同层级的下一节点,跨度记录了前进指针所指节点与当前节点的距离。
- BW(backward)指针:此属性位于zskiplistNode,表示指向当前节点的前一个节点。
- score:跳跃表表中根据节点的score大小升序排列。
- obj成员对象:节点保存的成员对象。
2.1跳跃表节点
下面为zskiplistNode的结构定义:
typedef struct zskiplistNode{
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel{
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
}zskiplistNode;
-
层
从上结构可看出,节点中包含着一个level数组,数组中可以包含多个元素,每个元素又有一个指向其它节点的指针,跳跃表正是利用这些指针快速访问其他节点。
在跳跃表创建时,会随机生成1~32之间的值作为level数组的大小。
-
前进指针
每层都有一个指向表尾方向的前进指针,用于从表头方向访问节点。
借用图1阐述跳跃表的遍历过程:
首先是从表头出发,从高层开始寻找下一个节点,从L32一直找到L4才找到指向o1的指针;
然后遍历到o1,o1的L4并没有指向下一个节点o2,然后向下找直到L1;
接着通过L1一直遍历直到指针指向null。
-
跨度
跨度用于记录两个节点的距离(指向null的指针跨度均为0)。在查找的过程中,跨度往往用于计算排位,当沿途访问的所有层的跨度累加,结果就是目标节点在跳跃表的排位。
继续看图1,当我们要求找o2时,从表头开始经过了两个跨度为1的节点,那么o2在整个跳跃表的排位为2。
-
后退指针
后退指针用于从表尾向表头方向访问节点,这么一说可能会有些“非人话”,简单地举几个例子,例如反向遍历跳跃表就可以从tail指向尾指针从后利用backward指针向前遍历;在例如查找o2节点,从高level向低level检索,先是走L5到o3,再利用BW后退从而找到o2。
-
分值和成员
分值可以理解为节点的级别,跳跃表中节点均是根据分值的大小升序排列的。
成员其实是一个指向SDS的指针。
不同节点的score可以相同,如图2所示:
所以,准确地说跳跃表的排列规则为o1<=o2<=o3。
2.2跳跃表
下面为zskiplist的结构定义:
typedef struct zskiplist{
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表的节点数量
unsigned long length;
// 表中节点的最大层数
int level;
} zkiplist;
从上结构可知,跳跃表访问表头节点和表尾节点的时间复杂度为O(1),返回表长length也是O(1),需要注意的是level计算的层高是排除表头节点。
后记
跳跃表查找元素的过程详见后退指针的例子。
由于本人能力有限,如有不当或错误的地方,欢迎大家批评指正。
微信搜——Ross的记录空间,欢迎关注。
我是Ross,我们下次再见。
参考
《Redis设计与实现》