理解数据结构Redis——跳跃表(跳表)

有序链表

  • 每个节点都有指向下一个节点的next指针,每次查找元素都需要从头节点开始。
  • 如果next节点值大于要查找的值或next指向null,则从当前节点降下一层,继续向后查找,是否可以提升查找效率?

分层有序链表(一般默认3层)

  • 分层有序链表如下图所示,假设我们查找值为51的节点,查找步骤如下。
    在这里插入图片描述
  1. 从第2层开始,1节点比51节点小,向后比较
  2. 21节点比51节点小,继续向后比较。第2层21节点的next指向null,所以下降一层
  3. 第1层中,21节点的next为41节点,继续向后比较。41节点的next为61节点,大于51节点,所以下降一层继续向后比较
  4. 第0层中,51节点为要查询的节点,返回结果
  • 总结:通过将有序链表分层,由最上层依次向后查询,如果本层的next节点大于要找的值或指向null,则从本节点开始,下降一层向后查找,依次类推,如果找到则返回节点,否则返回空。因此,在节点较多的时候可以跳过一些节点,查询效率大大提升,这就是跳跃表的基本思想

跳跃表

  • 跳跃表节点的结构
typedef struct zskiplistNode {
	sds ele; // 存储字符串类型数据
	double score; // 存储排序的分值
	struct zskiplistNode *backward; // 指向当前节点的前一个节点,向后遍历跳跃表时使用
	struct zskiplistLevel {
		struct zskiplistNode *forward; // 指向本层下一个节点
		unsigned int span; // 指向节点与本节点之间的个数
	} level[]; // 柔性数组,每个节点的数组长度不一样,随机生成一个1-64的值,值越大概率越低
} zskiplistNode;
  • 跳跃表的结构
typedef struct zskiplist {
	struct zskiplistNode *header, *tail;// 头节点和尾节点
	unsigned long length; // 跳表长度
	int level; // 跳表高度
} zskiplist;

跳跃表插入节点的4个步骤

第一步:查找要插入的位置

  • 跳表如下图所示。长度为3,高度为2。若要插入节点31,高度为3。
    在这里插入图片描述
  • 插入节点代码如下所示
x = zsl->header;
for (i = zsl->level - 1; i >= 0; i--) {
	rank[i] = i == (zsl->level - 1) ? 0 : rank[i + 1];
	while (x->level[i].forward && 
			(x->level[i].forward->score < score ||
				(x->level[i].forward->score == score &&
					sdscmp(x->level[i].forward->ele,ele) < 0)))
		{
			rank[i] += x->level[i].span;
			x = x->level[i].forward;
		}
		update[i] = x;
}
  • update[] : 插入节点时,需要更新被插入节点每层的前一个节点。由于每层更新的节点不同,所以需要将更新的节点记录在update[i]中。
  • rank[] : 记录当前层从header节点到update[i]节点所经历的步长,在更新update[i]的span和设置新插入节点的span时会用到。
  • 查找节点(score=31, level = 3)的插入位置,逻辑如下:
  1. 第一次for循环,i = 1。 x为跳跃表的头节点。
  2. 此时i的值与zsl->level-1相等,所以rank[1]的值为0。
  3. header->level[1].forward存在,并且header->level[1].forward->score==1小于31,所以可以进入while循环,rank[1] = 1,x为第一个节点
  4. 第一个节点的第一层的forward指向null,所以不会再进入while。经过第一次for循环,rank[1]=1。x和update[1]都为第一个节点(score=1)。
  5. 经过第二次for循环,i=0。x为跳跃表的第一个节点(score=1)
  6. 此时i的值与zsl->level-1不相等,所以rank[0]等于rank[1]的值,值为1
  7. x->level[0]->forward存在,并且x->level[0].foreard->score==21小于31,所以进入while循环,rank[0]==2。x为第二个节点(score=21)
  8. x->level[0]->forward存在,并且x->level[0].foreard->score==41大于31,所以不会进入while,经过第二次循环,rank[0]=2。x和update[0]都为第二个节点(score=21)
  • update和rank复制后的跳跃表如下图所示。
    在这里插入图片描述

第二步:调整跳跃表高度

  • 由第一步可知插入节点高度为3,大于跳跃表高度2,因此需要调整跳表高度。
level = zslRandomLevel()for (i = zsl->level; i < level; i++ ) {
	rank[i] = 0;
	update[i] = zsl->header;
	update[i]->level[i].span = zsl->length;
} 
zsl->level = level;
  • 此时,i的值为2,level值为3,所以只能进入一次for循环。由于header的第0层到第1层的forward都已经指向相应的节点,而新添加的节点高度大于跳表的原高度,所以第2层只需要更新header节点即可。rank用来更新span的变量,其值是头节点到update[i]的节点数,此次修改的是头节点,所以rank[2]为0,update[2]一定为头节点。update[2]->level[2].span的值先赋值为跳跃表的总长度,后续在计算新插入节点level[2]的span时会用到此值。
  • 调整高度后跳表如下图所示
    在这里插入图片描述

第三步:插入节点

  • 当update和rank都已赋值且节点都创建好后,便可以插入节点。
x = zslCreateNode(level, score, ele);
for (i = 0; i < level; i++) {
	x->level[i].forward = update[i]->level[i].forward;
	update[i]->level[i].forward = x;
	x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
	update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
  • level值为3,所以可以执行3次for循环,过程如下。
  1. 第一次for循环
    1.1 x的leve[0]的forward为update[0]的level[0]的forward节点,即x->level[0].forward为score=41的节点
    1.2 update[0]的level[0]的下一个节点为新插入的节点
    1.3 rank[0]-rank[0]=0,update[0]->level[0].span=1,所以x->level[0].span=1。
    1.4 update[0]->level[0].span = 0 + 1 = 1
  • 插入节点并更新0层后,跳表如下所示
    在这里插入图片描述
  1. 第2次for循环
    2.1 x的level[1]的forward为update[1]的level[1]的forward节点,即x->level[1].forward为null。
    2.2 update[1]的level[1]的下一个节点为新插入的节点
    2.3 rank[0]-rank[1] =1,update[1]->level[1].span=2,所以x->level[1].span=1
    2.4 update[1]->level[1].span=1+1=2
  • 插入节点并更新第1层后的跳表如下所示。
    在这里插入图片描述
  1. 第3次for循环
    3.1 x的level[2]的forward为update[2]的level[2]的forward节点,即x->level[2].forward为null。
    3.2 update[2]的level[2]的下一个节点为新插入的节点。
    3.3 rank[0]-rank[2]=2,因为update[2]->level[2].span=3,所以x->level[2].span=1
    3.4 update[2]->level[2].span=2+1=3
  • 插入节点并更新第2层后的跳表如下所示。
    在这里插入图片描述

第4步:调整backward

  • 根据update的赋值过程,新插入节点的前一个节点一定是update[0],由于每个节点的后退指针只有一个,于此节点的层数无关,所以当插入节点不是最后一个节点时,需要更新被插入节点的backward指向update[0]。如果新插入节点是最后一个节点,则需要更新跳表的尾节点为插入新节点。插入节点后,更新跳表长度+1.
  • 插入新节点后的跳表如下所示。
    在这里插入图片描述
  • 摘取自《Redis5设计与源码分析》
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值