Redis数据结构 -- 跳跃表

246 阅读3分钟

跳跃表(skiplist)是一种有序数据结构,它通过在每一个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。

 跳跃表支持平均时间复杂度为O(logN)、最坏O(N)的节点查找,还可以通过顺序性操作来批量处理节点(大部分情况,跳跃表的效率可以和平衡树相媲美,却更为简单)。

​ Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员(member)是比较长的字符串时,Redis就会使用跳跃表来作为有序集合的底层实现。

 本篇主要针对跳跃表的实现及数据结构、属性进行总结介绍。

跳跃表

整个跳跃表的结构如下图(图1):

跳跃表的结构

 备注:图1中连线上带有数字的箭头表示前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历是,访问会沿着层的前进指针进行。

 其属性如下:

  • header:指向跳跃表的表头节点
  • tail:指向跳跃表的表尾节点
  • level:记录跳跃表内,层数最大的节点的层数(表头节点层数不计算在内)。
  • length:记录跳跃表的长度,故获取跳跃表的长度时间复杂度为O(1).

跳跃表节点

位于zskiplist结构右方的是四个zskiplistNode结构,而每个跳跃表节点zskiplistNode结构,其结构如下图(图2):

跳跃表节点

其代码实现如下:

typedef struct zskiplistNode{
    // 后退指针
    struct zskiplistNode *backward;
    // 分值
    double score;
    // 成员对象
    robj *obj;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];
} zskiplistNode;

解析:

  • :level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度。层数越多,访问其他节点的速度越快。

    每创建新的zskiplistNode时,程序使用幂次定律(power law, 越大的数出现的概率越小),随机生成一个介于1-32的值作为level数组的大小,即 高度

  • 跨度:用于记录两个节点的距离。遍历操作只使用前进指针即可,跨度实际上是用来计算排位的:在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表的排位。

  • 后退指针:用于从表尾向表头方向访问节点:跟可以一次跳过多个节点的前进指针不同,因为每个节点只有一个后退指针,所以每次只能后退至前一个节点。

  • 分值和成员:分值是一个浮点数,跳跃表中所有节点都按分值从小到大排序。成员指向一个SDS对象。

    同时,各节点之间成员必须是唯一的,但是各节点保存的分值可以是相同的。排序:分值--成员大小, 小的在前,大的在后面

查找方式

比如输入命令 -- ZADD page_rank 10 google.com

  1. 通过上述方式生成--zskiplistNode
  2. 根据score查找需要插入的位置,通过遍历zskiplistLevel找到小于10的最大跨度level,比如找到level4找到节点o4,分值为8,那么还需要遍历节点o4的zskiplistLevel找到 <=2的最大跨度level,以此类推,找到指定的位置,让新增节点level的其中层指向下一个节点,前面节点指向新的节点,其时间复杂度为 O(log(N))。

如果从表尾向表头遍历跳跃表中的所有节点:

​ 程序先通过跳跃表的tail指针访问表尾节点,然后通过后退指针访问倒数第二个节点,之后再沿着后退指针访问倒数第三个节点,再后遇到指向NULLde后退指针,访问结束。

over~~~~~~~~~~~~~