- 跳跃表是一种有序的数据结构
- 每个节点都维护多个指向其他节点的指针,达到快速访问节点的目的
- 平均查找复杂度为O(logN)、最坏为O(N),可通过顺序操作批量处理接地那。
- 大部分情况其效率可和平衡树媲美
- 每个跳跃表层高都是1到32之间的随机数
跳跃表在redis中用于有序集合的实现。另外也用在集群节点的内部数据结构。
一、跳跃表的实现
zskiplist
该结构保存了跳跃表节点的信息。跳跃表节点由zskiplistNode
实现。
有如下属性:
-
header
指向跳跃表的表头节点
-
tail
指向表尾节点
-
level
跳跃表当前最大的层数的节点的层数(除表头之外)
-
length
跳跃表长度。即除表头之外的节点数量
因此直接定位表头和表尾节点的复杂度为O(1)。获取节点个数的复杂度也为O(1)
二、跳跃表节点
表节点由zskiplistNode实现。
typedef struct zskiplistNode{
//前进指针
struct skiplist *backward;
//分值
double score;
//成员对象
robj *obj;
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
unsigned int span;//跨度
}level[];
}zskiplistNode;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-A0aHIqcL-1588696729636)(assets/1588391991791.png)]
2.1后退指针
每个节点只有一个后退指针,因此只能一个个节点从后往前遍历。【上图的BW表示】
首先通过tail指针找到尾节点,再从尾节点的后退指针一步步往前遍历,直到遇到NULL,结束遍历。
而下面要讲的前进指针,每个节点可以有多个层,每个层都有一个前进指针,每个前进指针可以跨节点指向。
2.2 层
一个level[]数组表示。
2.2.1 前进指针
每层维护一个前进指针:指向位于表尾方向的其他节点,用于从表头向表尾方向访问节点。
- 首先访问表头节点,再从第四层的前进指针移到第二个节点
- 在第二个节点再从第二层的前进指针走到第三个节点
- 在第三个节点再从第二层的前进指针走到最后一个节点,即尾节点
- 从最后一个节点的第二层的前进指针移动,发现是NULL,就知道已经到表最后了,结束遍历。
2.2.2 跨度
跨度用于记录该层与前进节点连接的另一个节点的距离。
指向NULL的跨度为0.
如上图,可以清晰看出各个节点的各个层次的跨度。
跨度主要是用于计算排名rank的。当查找某个节点时,会通过前进指针进行查询,同时会将经过的所有层的跨度相加,就可以得到目标节点在跳跃表中的排名。
如上图,如果要查询对象o2,就要经过两个跨度为1的层,得到其排名为2。
2.3 分值和obj
节点属性中的score就是分值,在跳跃表中所有节点按照分值从小到大排序。
obj存储一个成员,为一个指针,指向一个字符串对象。
节点的成员必须唯一,但分值可以相同。分值相同的成员按字典中顺序排序。
如果一个集合中的所有成员都是整数值,则redis使用整数集合作为底层实现。
三、整数集合的实现
可保存类型为int16_t、int32_t或int64_t的整数值,且不会出现重复元素。
typedef struct intset{
//编码
uint32_t encoding;
//元素数量
uint32_t length;
//元素数组
int8_t contents[];
}intset;
contents[]数组保存一个整数集合中的所有元素,且按序存储。
数组的真正类型取决于encoding值。
四、升级
如果新添加的某个元素长度比其他几个都长,则所有元素都会升级到最长的这个元素的类型,如:-267738891737722773,1,3,4
虽然只有-267738891737722773是要用int64_t类型,其他三个只要int16_t就可以,但全部会升级为int64_t类型。
升级步骤:
- 根据新元素类型,扩展整个整数集合底层数组的空间大小,并为新元素分配控件
- 将底层数组所有元素转为与新元素相同的类型,并将转换类型后的元素放到正确的位置上,并保持有序性。
- 将新元素添加到数组的最后。
升级的好处
提升整数集合的灵活性
因为可以自动升级,所以可以随意的添加整数到集合,底层都会自动升级到合适的类型。
尽可能节约内存。
只有在需要升级的时候才会升级,如果所有元素都是一个类型,就不会进行升级。
四、降级
不支持降级,升级后会一直保持升级后的类型。