跳跃表
跳跃表是一种有序的数据结构,他通过在在每个结点中维护多个指向其他节点的指针,从而达到快速访问的目的。
跳跃表支持平均 O ( l o g N ) O(logN) O(logN),最坏 O ( N ) O(N) O(N)复杂度的节点查询,所以可以支持顺序性的操作批量去处理节点。
在大部分情况下,跳跃表的效率跟平衡树差不多,但实现起来比平衡树简单。
跳跃表是Redis中有序集合键的底层(也就是ZSet)。
Redis只在两个地方使用了跳跃表,一个是实现有序集合,另一个是在集群节点中用作内部数据结构 ,除此之外,没有任何用途。
跳跃表的实现
Redis的跳跃表由redis.h/zskiplistNode(节点)和redis.h/zskiplist(跳跃表)两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,zskiplistNode结构则用于保存跳跃表节点的相关信息,比如节点数量,以及指向表头结点和表尾结点的指针等等。
跳跃表本质上可以理解是一个有层数的链表
最左边的Head就是zskiplist结构,该结构包含以下属性
- header:指向第一个跳跃表的表头结点(是一个傀儡节点,没有什么用)
- tail:指向最后一个跳跃表的表尾结点(即指向末尾最后的数据节点)
- Level:记录目前跳跃表内,层数最大的那个节点的层数(不包括表头)
- length:记录跳跃表的长度,即跳跃表目前包含节点的数量(即数据节点数量)
位于zskiplist结构的右边3个是zskiplistNode结构,该结构包含以下属性
- 层:结点使用L1、L2、L3等字样标记结点的各个层,L1代表第一层,L2代表第二层,以此类推,每一层就是一个链表,链表里面的结点,拥有两个属性,一个是前进指针,一个是跨度(前进指针就是下一个结点的位置,跨度就是要跨越几个zskiplistNode才能到下一个结点,上图中的栗子跨度都为1,一般都不会这么均匀,越高层的索引链表跨度越大,除了最后一个结点的跨度为0,因为指向了NULL)。
- 后退指针(BW):BW指向当前结点的上一个结点,当从表尾遍历表头时使用。
- 分值(score,我这里用VALUE来表示)就是插入数据的键值对的value值。
- 成员对象(obj,我这里KEY用表示)就是插入数据的键值对的key值。
其实zskiplist是利用了数组来实现跳跃表的同一列指向的score相同,用数组来竖向存储指向同一score的索引,然后数组里面的结点元素再各自形成链表,称为level层索引单向链表。
找元素的过程如下,比如,这里我要找最后一个zskiplistNode结构的key。
此时先找到第一个傀儡结点,然后找到最高层的索引链表(我觉得应该是根据跨度来判断找出最高层的索引列表),傀儡结点找到第一个结点,然后开始对比key值(key值是按升序去排列的),通过对比发现当前结点的score小于要找的score,则在移动到当前链表的下一个结点,发现没有结点,通过当前结点的数组,竖向找下一层索引链表,此时找到下一层索引链表的对应结点的score值是相同的,因为都在同一个zskiplistNode结点内,然后去对比这一层索引链表的下一个结点的key,如果找到,就直接取该结点的value,找不到就依次到下一层索引链表。
跳跃表结点
跳跃表结点由redis.h/zskiplistNode结构定义
typedef struct zskiplistNode{
//后退指针
struct zskiplistNode *backward;
//分值key
double score;
//成员对象value
robj *obj;
//层(也是一个结构体,不过是一个数组)
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
}zskiplistNode;
层
跳跃表结点的level数组可以包含多个元素(不过越前面的结点,包含的元素一般会更多,因为要保证符合跳跃表的要求,即每一层的索引链表必须是下一层的索引链表的子集),一般来说,层越高,找元素的效率就越快。
每次创建一个结点的时候,redis会根据幂次定律(越大的数出现的机率越小)随机生成一个介于1和32之间的值,作为Level数组的大小(也就是这个结点的所拥有的索引高度,即上面有多少个索引链表会可以找到他)
前进指针
每个层里面的元素都有一个指向表尾的指针,用来找到下一个结点(对比key后,发现小了,要往前找,如果碰到NULL,就代表元素不存在)。
跨度
记录两个结点间的距离
- 两个结点之间的跨度越大,相距就越远
- 指向NULL的前进指针的跨度都为0
其实跨度的用途是用来计算结点的排位(rank)的,也就是结点的位置,排在最底层的第几个,找到结点的路径遇到的所有层的所有跨度加起来就是结点的排位。
后退指针
每个结点都有后退指针,用来从表尾遍历到表头的。
分值和成员
结点的分值就是存储键值对的value值,是一个double类型的浮点数,结点的顺序是按照value的大小来进行排序的(所以value必须可以解析为浮点型,而且必须唯一,如果重复的话会发生替换)
结点的成员对象是一个指针,指向的对象是一个sds对象(之前提到的SDS字符串),用来储存键值对的key值。
跳跃表
多个跳跃结点就可以形成跳跃表,并且仅仅通过一个zskiplist结构来持有这些跳跃结点。
typedef struct zskiplist{
//表头结点和表尾结点
struct skiplistNode *header,*tail;
//表中结点数量
unsigned long length;
//表中层数最大结点的层数(即拥有的索引链表数量)
int level;
}zskiplist;
header和tail指针分别指向跳跃表的表头和表尾结点,通过这两个指针,程序找到第一个结点和最后一个结点的时间复杂度都为 O ( 1 ) O(1) O(1),并且得到跳跃表的结点个数或者长度,直接返回length即可,时间复杂度也为 O ( 1 ) O(1) O(1)。
注意,LEVEL属性是不包括傀儡结点的,傀儡结点里面的数组有很多元素结点没有形成链表,即跨度为0
重点
- 跳跃表是有序集合的底层实现之一
- Redis的跳跃表由zskiplist和zskiplistNode组成,前者保存跳跃表信息,后者是跳跃表结点
- 每个跳跃表结点的层高为1~32
- 同一个跳跃表中,多个结点可以包含相同的分值(即score可以相同,score必须可以转化为浮点型!),但每个结点的成员对象(一个sds字符串)必须唯一(member必须唯一,出现重复会重置)
- 跳跃表的结点按照分值进行排序,如果出现分值相同,就按照成员对象的大小进行排序