跳跃表是一种有序的数据结构,支持平均O(logN)、最坏O(N)复杂度的节点查找。跳跃表应用在有序集合键和集群节点的场景上。本文参考Redis3.0版本的源码,注释参考了黄建宏的注释,并加上自己的理解。对于跳跃表和节点的定义是在redis.h中,而常用API的实现是在t_zset.c中。

定义:

/** 跳跃表*/
typedef struct zskiplist {// 表头节点和表尾节点struct zskiplistNode *header, *tail;// 表中节点的数量,不包含头节点unsigned long length;// 表中层数最大的节点的层数,不包含头节点int level;} zskiplist;
/* ZSETs use a specialized version of Skiplists */
/** 跳跃表节点*/
typedef struct zskiplistNode {// 成员对象robj *obj;// 分值double score;// 后退指针struct zskiplistNode *backward;// 层struct zskiplistLevel {// 前进指针struct zskiplistNode *forward;// 跨度unsigned int span;} level[];} zskiplistNode;

以下为一个可能的跳跃表示例:

新建:zslCreate

/** 创建并返回一个新的跳跃表** T = O(1)*/
zskiplist *zslCreate(void) {int j;zskiplist *zsl;// 分配空间zsl = zmalloc(sizeof(*zsl));// 设置高度和起始层数zsl->level = 1;zsl->length = 0;// 初始化表头节点// T = O(1)zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {zsl->header->level[j].forward = NULL;zsl->header->level[j].span = 0;}zsl->header->backward = NULL;// 设置表尾zsl->tail = NULL;return zsl;
}
/** 创建一个层数为 level 的跳跃表节点,* 并将节点的成员对象设置为 obj ,分值设置为 score 。** 返回值为新创建的跳跃表节点** T = O(1)*/
zskiplistNode *zslCreateNode(int level, double score, robj *obj) {// 分配空间zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));// 设置属性zn->score = score;zn->obj = obj;return zn;
}

插入:最为核心的API

/** 创建一个成员为 obj ,分值为 score 的新节点,* 并将这个新节点插入到跳跃表 zsl 中。* * 函数的返回值为新节点。*/
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {//这个update很巧妙,记录了离插入位置最近的那个节点,保存的是level[i].forward//如果在跳跃表上跟踪记录轨迹,则是竖折形状。zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; //32unsigned int rank[ZSKIPLIST_MAXLEVEL];int i, level;redisAssert(!isnan(score));// 在各个层查找节点的插入位置x = zsl->header;for (i = zsl->level-1; i >= 0; i--) {/* store rank that is crossed to reach the insert position */// rank[i]用来记录第i层达到插入位置的所跨越的节点总数,也就是该层最接近(小于)给定score的排名 // rank[0]则是离插入位置最近的节点的rank,是前面每一层最终的累加值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 &&// 比对成员, T = O(N)compareStringObjects(x->level[i].forward->obj,obj) < 0))) {// 记录沿途跨越了多少个节点rank[i] += x->level[i].span;// 移动至下一指针x = x->level[i].forward;}// 记录将要和新节点相连接的节点update[i] = x;}/* we assume the key is not already inside, since we allow duplicated* scores, and the re-insertion of score and redis object should never* happen since the caller of zslInsert() should test in the hash table* if the element is already inside or not. ** zslInsert() 的调用者会确保同分值且同成员的元素不会出现,* 所以这里不需要进一步进行检查,可以直接创建新元素。*/// 获取一个随机值作为新节点的层数// T = O(N)level = zslRandomLevel();// 如果新节点的层数比表中其他节点的层数都要大// 那么初始化表头节点中未使用的层,并将它们记录到 update 数组中// 将来也指向新节点if (level > zsl->level) {// 初始化未使用层// T = O(1)for (i = zsl->level; i < level; i++) {rank[i] = 0;//初始化头节点中未触及到的区间[zsl->level,level)update[i] = zsl->header;update[i]->level[i].span = zsl->length; //这个没看明白}// 更新表中节点最大层数zsl->level = level;}// 创建新节点x = zslCreateNode(level,score,obj);// 将前面记录的指针指向新节点,并做相应的设置// T = O(1)for (i = 0; i < level; i++) {// 设置新节点的 forward 指针x->level[i].forward = update[i]->level[i].forward;// 将沿途记录的各个节点的 forward 指针指向新节点update[i]->level[i].forward = x;/* update span covered by update[i] as x is inserted here */// 计算新节点跨越的节点数量// 未插入前顺序:update[i]..update[0]   插入x后顺序: update[i]..update[0]..x  // rank[0]-rank[i]表示的是update[i]和update[0]之间的跨度span// update[i]->level[i].span表示的是update[i]与update[i]->level[i]->forward之间的span x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);// 更新新节点插入之后,沿途节点的 span 值// 其中的 +1 计算的是新节点,表示时从update[i]->level[i]到x的spanupdate[i]->level[i].span = (rank[0] - rank[i]) + 1;}/* increment span for untouched levels *///如果新节点的level小于跳跃表的最大层数,未接触的节点的 span 值也需要增一,因为横跨在新节点上方,这些节点直接从表头指向新节点// T = O(1)for (i = level; i < zsl->level; i++) {update[i]->level[i].span++;}// 设置新节点的后退指针// 新节点可能直接插在头节点的后面,这种情况下update[0]为headerx->backward = (update[0] == zsl->header) ? NULL : update[0];// 插入位置是否插入尾节点if (x->level[0].forward) x->level[0].forward->backward = x;elsezsl->tail = x;// 跳跃表的节点计数增一zsl->length++;return x;
}
/* Returns a random level for the new skiplist node we are going to create.** 返回一个随机值,用作新跳跃表节点的层数。** The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL* (both inclusive), with a powerlaw-alike distribution where higher* levels are less likely to be returned. ** 返回值介乎 1 和 ZSKIPLIST_MAXLEVEL 之间(包含 ZSKIPLIST_MAXLEVEL),* 根据随机算法所使用的幂次定律,越大的值生成的几率越小。** T = O(N)*/
int zslRandomLevel(void) {int level = 1;while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))level += 1;return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

特别注意的是,这个层数创建时是根据幂次定律来随机生成一个1-32之间的值。具体算法参见随机算法。

删除

/* Delete an element with matching score/object from the skiplist. ** 从跳跃表 zsl 中删除包含给定节点 score 并且带有指定对象 obj 的节点。** T_wrost = O(N^2), T_avg = O(N log N)*/
int zslDelete(zskiplist *zsl, double score, robj *obj) {zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;int i;// 遍历跳跃表,查找目标节点,并记录所有沿途节点// T_wrost = O(N^2), T_avg = O(N log N)x = zsl->header;for (i = zsl->level-1; i >= 0; i--) {// 遍历跳跃表的复杂度为 T_wrost = O(N), T_avg = O(log N)while (x->level[i].forward &&(x->level[i].forward->score < score ||// 比对分值(x->level[i].forward->score == score &&// 比对对象,T = O(N)compareStringObjects(x->level[i].forward->obj,obj) < 0)))// 沿着前进指针移动x = x->level[i].forward;// 记录沿途节点update[i] = x;}/* We may have multiple elements with the same score, what we need* is to find the element with both the right score and object. ** 检查找到的元素 x ,只有在它的分值和对象都相同时,才将它删除。*/x = x->level[0].forward;if (x && score == x->score && equalStringObjects(x->obj,obj)) {// T = O(1)zslDeleteNode(zsl, x, update);// T = O(1)zslFreeNode(x);return 1;} else {return 0; /* not found */}return 0; /* not found */
}

内部删除:

/* Internal function used by zslDelete, zslDeleteByScore and zslDeleteByRank * * 内部删除函数,* 被 zslDelete 、 zslDeleteRangeByScore 和 zslDeleteByRank 等函数调用。** T = O(1)* 输入update为与删除节点x相关的前置节点数组,与插入操作的中的update类似*/
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {int i;// 更新所有和被删除节点 x 有关的节点的指针,解除它们之间的关系// 两种情况:// (1)与删除节点x直接相连的:相加减1// (2)横跨删除节点的:直接减1// T = O(1)for (i = 0; i < zsl->level; i++) {if (update[i]->level[i].forward == x) {update[i]->level[i].span += x->level[i].span - 1;update[i]->level[i].forward = x->level[i].forward;} else {update[i]->level[i].span -= 1;}}// 更新被删除节点 x 的前进和后退指针:非尾节点和尾节点分别处理if (x->level[0].forward) {x->level[0].forward->backward = x->backward;} else {zsl->tail = x->backward;}// 更新跳跃表最大层数(只在被删除节点是跳跃表中最高的节点时才执行)// T = O(1)// 遍历头节点的level数组,直到找到第一个前进指针不为NULL的while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)zsl->level--;// 跳跃表节点计数器减一zsl->length--;
}

zslGetRank:

/* Find the rank for an element by both score and key.** 查找包含给定分值和成员对象的节点在跳跃表中的排位。** Returns 0 when the element cannot be found, rank otherwise.** 如果没有包含给定分值和成员对象的节点,返回 0 ,否则返回排位。** Note that the rank is 1-based due to the span of zsl->header to the* first element. ** 注意,因为跳跃表的表头也被计算在内,所以返回的排位以 1 为起始值。** T_wrost = O(N), T_avg = O(log N)*/
unsigned long zslGetRank(zskiplist *zsl, double score, robj *o) {zskiplistNode *x;unsigned long rank = 0;int i;// 遍历整个跳跃表x = zsl->header;for (i = zsl->level-1; i >= 0; i--) {// 遍历节点并对比元素while (x->level[i].forward &&(x->level[i].forward->score < score ||// 比对分值(x->level[i].forward->score == score &&// 比对成员对象compareStringObjects(x->level[i].forward->obj,o) <= 0))) {// 累积跨越的节点数量rank += x->level[i].span;// 沿着前进指针遍历跳跃表x = x->level[i].forward;}/* x might be equal to zsl->header, so test if obj is non-NULL */// x可能为空节点,需要进行非空判断。必须确保不仅分值相等,而且成员对象也要相等// T = O(N)if (x->obj && equalStringObjects(x->obj,o)) {return rank;}}// 没找到return 0;
}

参考文献:

https://blog.csdn.net/men_wen/article/details/70040026

https://blog.csdn.net/kisimple/article/details/38706729

http://makaidong.com/yongning99/1/88707_10585499.html

Redis跳跃表源码解析相关推荐

  1. Redis源码-String:Redis String命令、Redis String存储原理、Redis String三种编码类型、Redis字符串SDS源码解析、Redis String应用场景

    Redis源码-String:Redis String命令.Redis String存储原理.Redis String三种编码类型.Redis字符串SDS源码解析.Redis String应用场景 R ...

  2. Redis radix tree源码解析

    Redis实现了不定长压缩前缀的radix tree,用在集群模式下存储slot对应的的所有key信息.本文将详述在Redis中如何实现radix tree. 核心数据结构 raxNode是radix ...

  3. jtoken判断是否包含键_Redis源码解析十三--有序集合类型键实现(t_zset)

    有序集合类型键实现 1. 有序集合命令 Redis有序集合命令如下表所示:Redis 有序集合命令详解 2. 有序集合类型实现 有序集合对象的底层实现类型如下表: 关于底层的数据结构剖析和实现,请看如 ...

  4. arcengine遍历属性表_Redis源码解析四--跳跃表

    Redis 跳跃表(skiplist) 1. 跳跃表(skiplist)介绍 定义:跳跃表是一个有序链表,其中每个节点包含不定数量的链接,节点中的第i个链接构成的单向链表跳过含有少于i个链接的节点. ...

  5. Redis源码解析(15) 哨兵机制[2] 信息同步与TILT模式

    Redis源码解析(1) 动态字符串与链表 Redis源码解析(2) 字典与迭代器 Redis源码解析(3) 跳跃表 Redis源码解析(4) 整数集合 Redis源码解析(5) 压缩列表 Redis ...

  6. Redis源码解析——双向链表

    相对于之前介绍的字典和SDS字符串库,Redis的双向链表库则是非常标准的.教科书般简单的库.但是作为Redis源码的一部分,我决定还是要讲一讲的.(转载请指明出于breaksoftware的csdn ...

  7. Redis源码解析——字典基本操作

    有了<Redis源码解析--字典结构>的基础,我们便可以对dict的实现进行展开分析.(转载请指明出于breaksoftware的csdn博客) 创建字典 一般字典创建时,都是没有数据的, ...

  8. Redis源码解析——内存管理

    在<Redis源码解析--源码工程结构>一文中,我们介绍了Redis可能会根据环境或用户指定选择不同的内存管理库.在linux系统中,Redis默认使用jemalloc库.当然用户可以指定 ...

  9. Redis源码解析——前言

    今天开启Redis源码的阅读之旅.对于一些没有接触过开源代码分析的同学来说,可能这是一件很麻烦的事.但是我总觉得做一件事,不管有多大多难,我们首先要在战略上蔑视它,但是要在战术上重视它.除了一些高大上 ...

  10. Redis进阶- Redisson分布式锁实现原理及源码解析

    文章目录 Pre 用法 Redisson分布式锁实现原理 Redisson分布式锁源码分析 redisson.getLock(lockKey) 的逻辑 redissonLock.lock()的逻辑 r ...

最新文章

  1. Html5 Canvas 扫雷 (IE9测试通过)
  2. YOLO-ReT让边缘端也可以实时检测
  3. 如何自行查找SAP ERP的物料主数据和CRM产品主数据的映射关系
  4. 面对焦虑,我们能做什么?
  5. 单分支 两路分支和多分支的if结构_JavaScript学习笔记(二)-- 分支结构
  6. mysql 子字符串_Mysql 截取字符串取子集的函数应用
  7. log4net进阶手札(四):保存自定义对象到oracle
  8. 期刊目录 核心期刊 计算机学术期刊等
  9. 信号处理 | 维纳滤波推导
  10. .net的commandname领悟
  11. 计算机地图制图符号制作的心得,计算机地图制图实习报告.docx
  12. ofdm信道估计 线性插值matlab,OFDM信道估计的MATLAB仿真
  13. python列表内存分配_python 列表, 元组内存分配优化
  14. 【天怒人怨爸爸系列】一年级数学口算题生成程序
  15. Leetcode高频题目整理(更新)
  16. NDK-r25交叉编译glib-2.73.3
  17. 【LaTex】第二行作者居中(IEEEtran模板)
  18. 如何成为智者:见微知著
  19. 淘宝粉丝增加技巧!京东店铺粉丝可以买?
  20. Visual Studio Code(VS)

热门文章

  1. html 表格横向排列,excel表格数据如何实现横向排列-Excel表格怎样把多列横向数据按照顺序改为纵向排列......
  2. 微信小程序实现当前页面多个视频文件只能播放一个视频,其他视频暂停,点击当前暂停当前
  3. 毕设 JAVA超市管理系统论文
  4. pd.to_datetime函数函数获取相应时间维度指标(isocalendar函数用法)
  5. python 离散数学 判断单射 双射 满射
  6. python条形码识别_使用Python和OpenCV在视频中实时监测条形码
  7. web数据可视化(ECharts版)
  8. 分析力学复习笔记(更新中)
  9. 内存屏障(Memory Barrier)(一)什么是写屏障?
  10. 【MobaXterm】设置 黑夜模式和白天模式