一起学Redis(3)——跳跃表

概念

Redis中又一个重要的数据结构——跳跃表。跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。大部分情况下,跳跃表的效率可以和平衡二叉树相媲美,并且跳跃表比平衡二叉树实现更简单,操作也更容易。Redis使用跳跃表作为有序集合的底层实现之一。举个例子:fruit-price是一个有序集合键,水果名为成员,价格为分值,保存了130款水果的价钱

redis-> ZRANGE fruit-price 0 2 WITHSCORES
1) "banana"
2) "5"
3) "cherry"
4) "6.5"
5) "apple"
6) "8"
redis-> ZCARD fruit-price
(integer) 130

跳跃表有redis.h/zskiplistNode 和 reids.h/zskiplist 两个结构定义,一个表示节点,一个存储跳跃表相关信息,稍后我们来看具体定义,先看一个结构图。
image

图片最左边是zskiplist结构,该结构包含以下属性:

  • header 指向跳跃表的表头节点
  • tail 指向跳跃表的表尾节点
  • level 记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)
  • length 记录跳跃表的长度,也就是跳跃表目前包含的节点的数量

右方的是四个zskiplistNode结构,包含以下属性:

  • 层(level)节点中用L1,L2,L3等字样标记节点的各个层,L1代表第一层,L2代表第二层,以此类推。每个层都有两个属性,前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离。在上面的图中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾遍历时,访问会沿着层的前进指针进行。
  • 后退(backward)指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
  • 分值(score):各个节点中1.0,2.0和3.0是节点所保存的分值,在跳跃表中,节点按各自所保存的分值从小到大排列。
  • 成员对象(obj):各个节点中的o1,o2和o3是节点所保存的成员对象
    表头节点和其他节点是一样的,也有后退指针,分值和成员对象,不过表头节点的这些属性都不会用到,所以图中省略了这些部分,只显示了表头节点的各个层。

节点实现

typedef struct zskiplistNode{

    struct zskiplistLevel {
        struct zskiplistNode * forward;
        unsigned int span;
    } level[];

    struct zskiplistNode * backward;
    double score;
    obj * obj;
} zskiplistNode;
  • 层:跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他几点的速度就越快。每次创建一个跳跃表节点的时候,程序都根据幂次定律(power law,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是这个层的高度。
  • 前进指针:用于遍历跳跃表,举个例子:
    • 迭代程序首先访问跳跃表的第一个节点(表头),然后从第四层的前进指针移动到表中的第二个节点。
    • 在第二个节点,程序沿着第二层的前进指针移动到表中的第三个节点
    • 在第三个节点,程序同样沿着第二层的前进指针移动到表中的第四个节点
    • 当程序再次沿着第四个节点的前进指针移动时,它碰到一个null,程序知道这是已经到达了跳跃表的表尾,于是结束这次遍历

image

  • 跨度:层的跨度用与记录两个节点之间的距离,看上去span好像与遍历有关,但是前面的遍历过程并没有用到这个属性,其实span是用来rank的,比如需要快速知道某个节点在跳跃表中排序,可以使用span计算得出。
  • 后退指针:每个节点只有一个后退指针,后退指针在跳跃表中用于后退操作,与前进指针不同,每次只能后退一个节点。
  • 分值和成员:分值时double类型,跳跃表按分值排序,成员时obj类型,指针指向SDS字符串。

跳跃表定义

typedef struct zskiplist {
    struct zskiplistNode * header, *tail;
    unsigned long length;
    int level;
} zskiplist;

看了定义,应该就知道这个是保存跳跃表相关信息的,header,tail指针用于快速操作跳跃表,长度也是记录节点的数量。level属性用于获取表中层最大的那个节点的层数量,注意表头的高度不算在内。这个属性在查询跳跃表上很重要,稍后介绍。

查询操作

前面说到,跳跃表查询效率平均为O(logN)最坏O(N)的时间复杂度,最好是什么情况最坏又是什么情况?
如果把跳跃表理解为平衡二叉树就很好理解复杂度的问题。把跳跃表的最高层想象为平衡二叉树的根节点,每次查询是从最高层出发,这个时候就用到了前面说的level属性,找到头节点相应的层,并移动到最高层的节点,判断其值是否时查询值,如果不是则根据大小关系,在平衡二叉树中大于目标值则向左,小于目标值向右子节点移动,跳跃表中相前相后移动,最终找到查询址,这样的话复杂度就和平衡二叉树一致,是O(logN)了,最坏情况就是最高节点是表尾节点,查询的是最小值,则复杂度为O(N),这样就很好理解了。

猜你喜欢

转载自blog.csdn.net/sinat_30333853/article/details/80210647