第五章 跳跃表
跳跃表 在每个节点维持多个指向其他节点的指针,达到快速访问节点的目的。Redis使用跳跃表作为有序集合键的底层实现之一。
5.1 跳跃表的实现
redis.h/zskiplistNode表示跳跃表节点。redis.h/zskiplist保存跳跃表节点的相关信息:节点数量、指向表头表尾节点的指针等。
zskiplist结构:
- header:指向跳跃表的表头节点
- tail:指向跳跃表的表尾节点
- level:记录目前跳跃表内,层数最大的那个节点层数(表头节点的层数不计算在内)
- length:表中节点的数量,不包含表头
zskiplistNode结构:
- level:用L1、L2....表示层。每层带有两个属性:前进指针和跨度。前进指针指向了一个可以访问的节点,跨度记录了当前节点和下一个可访问节点的距离。层数越多,访问其他节点就越快
- backward指针:指向前一个节点,从表尾向表头遍历时使用
- score:记录该节点代表的分值,如图中的1.0,2.0,3.0,所有节点按照分值从小到大排列
- obj:各个节点保存的成员对象,如图中o1,o2,o3
5.1.1 跳跃表节点
type struct zskiplistNode{ //后退指针 struct zskiplistNode *backward; //分值 double score; //成员对象 obj *obj; //层 struct zskiplistLevel{ //前进指针 struct zskiplistNode *forward; //跨度 unsigned int span; } level[]; }zskiplistNode;
1. 层
跳表节点的level数组包含多个元素,每个元素代表一层。层数大小在1~32之间随机生成。
2. 前进指针
通过每层的前进指针(level[i].forward)遍历整个调整的节点,直到遇到NULL。
3. 跨度
层的跨度(level[i].span)记录了该层指向的下一个节点的距离。指向NULL的层的跨度为0。在遍历的过程,记录访问路径上每个层数的跨度,当到达目的节点时,累加得到的跨度就是目的节点在跳跃表中的排位。
4. 后退指针
每个节点只有一个后退节点
5. 分值和成员
节点的分值(score属性)是一个double类型的浮点数,节点按照各自的分值从小到大排列
节点的成员对象(obj属性)是一个指针,它指向一个字符串对象,而字符串对象存放着一个SDS值
节点的分值可以相同,但成员对象必须唯一。当分值相同时,根据保存的字符串对象在字典中的顺序排序。
5.1.2 跳跃表
zskiplist是为了更方便的操作跳跃表,其中包含了可以访问跳表表头和跳表表尾的指针,或是快速获取跳表节点的数量。
5.2 跳跃表查找的时间复杂度以及跳跃表和红黑树的区别
查找时间复杂度为O(logN),根红黑树一致。区别是红黑树维护平衡需要旋转,操作复杂,而跳表设计简单,但需要更多的存储空间。
5.3 跳跃表插入和删除操作流程
5.3.1 插入流程
新节点从最高层开始逐层向下比较寻找,确定插入位置,复杂度O(logN)
将索引插入到原链表,复杂度O(1)
利用抛硬币的随机方式,决定新节点是否提升到上一级索引。若为正,新节点层数加一,并继续尝试,直到为负。时间复杂度为O(logN),但是Redis中限制了层数最多32,应该是O(1)
假设节点每层节点数是上一层的k倍,那么总节点数计算如下公式。比如当k为2时,总结点数为2N。这也说明了跳跃表,是一种用空间换时间的设计思路,相对于红黑树来说,操作简单,但需要的存储空间更多。
5.3.2 删除流程
从最高层向下比较寻找,找到节点后,删除节点,删除节点每层的时候,需要将前节点的当前层指针指向后节点的当前层,即注意维护层之间的指针关系。
参考: