第五章 跳跃表

跳跃表 在每个节点维持多个指向其他节点的指针,达到快速访问节点的目的。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 删除流程

  从最高层向下比较寻找,找到节点后,删除节点,删除节点每层的时候,需要将前节点的当前层指针指向后节点的当前层,即注意维护层之间的指针关系。

参考:

关于跳跃表的一点理解  

人生就像蒲公英,看似自由,其实身不由己。
原文地址:https://www.cnblogs.com/walker993/p/14425990.html