看完Redis源码还不理解跳跃表吗?( 四 )


下图分别展示了三个高度为 1 层、 3 层和 5 层的节点,因为 C 语言的数组索引总是从 0 开始的,所以节点的第一层是 level[0] ,而第二层是 level[1] ,以此类推 。

看完Redis源码还不理解跳跃表吗?

文章插图
 
前进指针每个层都有一个指向表尾方向的前进指针(level[i].forward 属性),用于从表头向表尾方向访问节点 。
看完Redis源码还不理解跳跃表吗?

文章插图
 
上图用虚线表示出了程序从表头向表尾方向,遍历跳跃表中所有节点的路径:
  1. 迭代程序首先访问跳跃表的第一个节点(表头),然后从第四层的前进指针移动到表中的第二个节点 。
  2. 在第二个节点时,程序沿着第二层的前进指针移动到表中的第三个节点 。
  3. 在第三个节点时,程序同样沿着第二层的前进指针移动到表中的第四个节点 。
  4. 当程序再次沿着第四个节点的前进指针移动时,它碰到一个 NULL ,程序知道这时已经到达了跳跃表的表尾,于是结束这次遍历 。
跨度层的跨度(level[i].span 属性)用于记录两个节点之间的距离:
  • 两个节点之间的跨度越大,它们相距得就越远 。
  • 指向 NULL 的所有前进指针的跨度都为 0 ,因为它们没有连向任何节点 。
初看上去,很容易以为跨度和遍历操作有关,但实际上并不是这样 —— 遍历操作只使用前进指针就可以完成了,跨度实际上是用来计算排位(rank)的: 在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表中的排位 。
举个例子,如下用虚线标记了在跳跃表中查找分值为 3.0 、 成员对象为 o3 的节点时,沿途经历的层: 查找的过程只经过了一个层,并且层的跨度为 3 ,所以目标节点在跳跃表中的排位为 3  。
看完Redis源码还不理解跳跃表吗?

文章插图
 
再举个例子,如下用虚线标记了在跳跃表中查找分值为 2.0 、 成员对象为 o2 的节点时,沿途经历的层: 在查找节点的过程中,程序经过了两个跨度为 1 的节点,因此可以计算出,目标节点在跳跃表中的排位为 2。
看完Redis源码还不理解跳跃表吗?

文章插图
 
后退指针节点的后退指针(backward 属性)用于从表尾向表头方向访问节点: 跟可以一次跳过多个节点的前进指针不同,因为每个节点只有一个后退指针,所以每次只能后退至前一个节点 。
看完Redis源码还不理解跳跃表吗?

文章插图
 
上图用虚线展示了如果从表尾向表头遍历跳跃表中的所有节点: 程序首先通过跳跃表的 tail 指针访问表尾节点,然后通过后退指针访问倒数第二个节点,之后再沿着后退指针访问倒数第三个节点,再之后遇到指向 NULL 的后退指针,于是访问结束 。
分值和成员
  • 节点的分值(score 属性)是一个 double 类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序 。
  • 节点的成员对象(obj 属性)是一个指针,它指向一个字符串对象,而字符串对象则保存着一个 SDS(简单动态字符串) 值 。
在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值却可以是相同的: 分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面(靠近表头的方向),而成员对象较大的节点则会排在后面(靠近表尾的方向) 。
举个例子,在下图所示的跳跃表中,三个跳跃表节点都保存了相同的分值 10086.0 ,但保存成员对象 o1 的节点却排在保存成员对象 o2 和 o3 的节点之前,而保存成员对象 o2 的节点又排在保存成员对象 o3 的节点之前,由此可见, o1 、 o2 、 o3 三个成员对象在字典中的排序为 o1 <= o2 <= o3  。
看完Redis源码还不理解跳跃表吗?

文章插图
 
跳跃表虽然仅靠多个跳跃表节点就可以组成一个跳跃表,如下图 所示:
看完Redis源码还不理解跳跃表吗?

文章插图
 


推荐阅读