【Redi设计与实现】第五章:跳跃表

228 阅读6分钟

跳跃表是一种有序的数据结构,他通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。 跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。

5.1 跳跃表的实现

Redis的跳跃表由 zskiplistNodezskiplist 两个结构定义。

其中,zskiplistNode 用于表示跳跃节点,而 zskiplist 结构则用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头节点和表尾节点的指针等等。

在这里插入图片描述

图5-1展示了一个跳跃表实例,位于图片最左边的是 zskiplist 结构,该结构包含一下属性:

  • header:指向跳跃表的头节点
  • tail:指向跳跃表的尾节点
  • level:记录目前跳跃表内,层数最大的那个节点的层数
  • length:记录跳跃表的长度

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

  • 层:节点中使用L1、L2 、L3等字样标记节点的每个层,L1代表第一层,以此类推。每个层都含有2个属性:前进指针和跨度。前进指针用于访问表尾方向的其他节点,而跨度记录了前进指针和跨度所指节点和当前节点的距离。如图5-1所示,连线上带有数字的箭头就代表了前进指针,而那个数字就是跨度。
  • 后退指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。
  • 分值:各个节点的1.0、2.0、3.0是节点保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。
  • 成员对象:各个节点的o1、o2、o3是节点所保存的对象。

注意表头节点和其他节点的构造是一样的:表头节点也有后退指针、分值、成员对象,不过表头节点的这些属性不会被用到,所以图中省略了这些部分,只显示了表头节点的各个层。

5.1.1 跳跃表节点

跳跃表节点的实现由 zskiplistNode 结构定义:

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

5.1.1.1 层

跳跃表的层数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快。

每次创建一个新跳跃节点的时候,程序都会根据幂次定律(越大的数出现的概率越小)随机生成一个介于1~32之间的值作为level数组的大小,这个大小就是层的高度。

随机生成的规律:每升高一级,出现概率下降一半

//随机产生层数  
int randomLevel()    
{  
    int k=1;  
    while (rand()%2)    
        k++;    
    k=(k<MAX_LEVEL)?k:MAX_LEVEL;  
    return k;    
}

5.1.1.2 前进指针

每个层都有一个指向表尾方向的前进指针(level[i].forward属性),用于从表头向表尾方向访问节点。 在这里插入图片描述

  • 程序首先访问第一个节点(表头),然后从第四层的前进指针移动到表中的第二个节点。
  • 在第二个节点时,程序沿着第二层的前进指针移动到列表中的第三个节点。
  • 在第三个节点时,程序同样沿着第二层的前进指针到达表中的第四个节点
  • 当程序到达第四个节点的时候,会碰到一个NULL,程序知道已经到达了跳跃表的表尾,于是结束这次遍历。

5.1.1.3 跨度

层的跨度主要记录两个节点之间的距离:

  • 两个节点之间的跨度越大,他们相距的就越远
  • 指向NULL的所有前进指针的跨度都为0,因为他们没有连接任何节点。

第一眼看上去,很容易以为跨度和遍历操作有关,但实际上不是这样的。

遍历我们只需要使用前进指针就可以完成了,跨度实际上是用来计算排名的:在查找某个节点的过程中,将沿途访问过的所有层的跨度加起来,得到的结果就是目标节点在跳跃表的排位。

5.1.1.4 后退指针

节点的后退指针用于从表尾向表头方向访问节点:前进指针的话,可以一次性跳过多个节点,而我们的后退指针,一次只能回退一个节点。

5.1.1.5 分值和成员

节点的分值(score属性)是一个double类型的浮点数,跳跃表中的所有节点都按照分值从小到大进行排序。

节点的成员对象(obj属性)是一个指针,指向一个字符串对象,而字符串对象则保存着一个SDS值。

在同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是各个节点保存的分值却可以是相同的:分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员较小的节点会排在前面(靠近表头的地方),而成员对象较大的节点则会排在后面(靠近表尾的方向)。

5.1.2 跳跃表

通过一个 zskiplist 结构来持有这些节点,程序可以方便的对整个跳跃表进行处理,比如:快速访问表头节点和表尾节点,或者快速的获取跳跃表的长度。

zskiplist 结构定义如下:

typedef struct zskiplist {
	// 表头节点和表尾节点
	struct skiplistNode *header,*tail;
	// 表示节点的数量
	unsigned long length;
	// 表中层数最大的节点的层数
	int level; 
} zskiplist;

headertail 指针分别指向跳跃表的表头和表尾节点,通过这两个指针,我们可以通过O(1)的时间复杂度来获取表头和表尾。

通过使用 length 属性来记录节点的数量,程序可以在O(1)的复杂度返回跳跃表的长度。

level 属性用于在O(1)的时间获取跳跃表层数最大的那个节点的层数量(不算表头的)。

5.2 跳跃表API

在这里插入图片描述

5.3 重点回顾

  • 跳跃表是有序集合的底层实现之一。
  • Redis 的跳跃表实现由 zskiplistzskiplistNode 两个结构构成,其中 zskiplist 属性保存跳跃表的信息(比如表头节点、表尾节点、长度),而 zskiplistNode 用于表示跳跃表节点。
  • 每个跳跃表节点的层高都是 1~32 之间的随机数。
  • 在同一个跳跃表中,多个节点可以包含相同的分值,但不能包含相同的对象。
  • 跳跃表节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。