【redis源码】跳跃表的实现

334 阅读4分钟

简介

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

redis用它来实现有序集合健(zset)

复杂度:平均O(logN) 最坏O(N)

跳跃表的结构

/*
 * 跳跃表
 */
typedef struct zskiplist {
  // 表头节点和表尾节点
  struct zskiplistNode *header, *tail;
  // 表中节点的数量
  unsigned long length;
  // 表中层数最大的节点的层数
  int level;
} zskiplist;
  • header和tail指针分别指向跳跃表的表头结点和表尾结点,通过这两个指针,定位表头结点和表尾结点的复杂度为O(1)。
  • 表尾结点是表中最后一个结点。而表头结点实际上是一个伪结点,该结点的成员对象为NULL,分值为0,它的层数固定为32(层的最大值)。
  • length属性记录结点的数最,程序可以在O(1)的时间复杂度内返回跳跃表的长度。
  • level属性记录跳跃表的层数,也就是表中层高最大的那个结点的层数,注意,表头结点的层高并不计算在内。
/*
 * 跳跃表节点
 */
typedef struct zskiplistNode {
  // 成员对象
  robj *obj;
  // 分值
  double score;
  // 后退指针
  struct zskiplistNode *backward;
  // 层
  struct zskiplistLevel {
    // 前进指针
    struct zskiplistNode *forward;
    // 跨度
    unsigned int span;
  } level[];
} zskiplistNode;
  • obj是该结点的成员对象指针(member),score是该对象的分值,是一个浮点数,跳跃表中的所有结点,都是根据score从小到大来排序的。
  • 同一个跳跃表中,各个结点保存的成员对象必须是唯一的,但是多个结点保存的分值却可以是相同的:分值相同的结点将按照成员对象的字典顺序从小到大进行排序。
  • level数组是一个柔性数组成员,它可以包含多个元素,每个元素都包含一个层指针(level[i].forward),指向该结点在本层的后继结点。该指针用于从表头向表尾方向访问结点。可以通过这些层指针来加快访问结点的速度。
  • 每次创建一个新跳跃表结点的时候,程序都根据幂次定律(power law,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是该结点包含的层数。
/*
 * 有序集合
 */
typedef struct zset {
    // 字典,键为成员,值为分值
    // 用于支持 O(1) 复杂度的按成员取分值操作
    dict *dict;
    // 跳跃表,按分值排序成员
    // 用于支持平均复杂度为 O(log N) 的按分值定位成员操作
    // 以及范围操作
    zskiplist *zsl;
} zset

添加member和score时会把member和score存入dict里,用来支持 O(1) 复杂度的按成员取分值操作

实现逻辑

1. 获取集合的长度O(1) zskiplist->length

2. 获取score值O(1) redis会把member和score存入dict里

3. zrank怎么取排名的? 时间复杂度:T_wrost = O(N), T_avg = O(log N)

redis会从header节点遍历整个跳跃表,从level层开始,沿着前进指针遍历 然后逐层递减遍历,期间会读取zskiplistNode->span 用变量rank记录累积跨越的节点数量,如果找到最终返回rank,没找到返回0

4. zadd新增节点 时间复杂度:T_wrost = O(N), T_avg = O(log N)

  • redis从header节点遍历整个跳跃表,从level层开始,沿着前进指针遍历,在各个层查找节点的插入位置,沿途会记录下跨越的节点数rank[i],和将要和新节点相连接的节点update[i]
  • 通过幂次定律获取一个随机值作为新节点的层数,最大32,然后创建新节点
  • 将前面记录的指针指向新节点,并做相应的设置,设置各个层的span,forward
  • 设置新节点的后退指针,跳跃表的节点计数增一

最后

个人公众号:技术源区

欢迎扫描下图关注公众号:技术源区,分享一些有深度的文章!