各位看官大大们,双节快乐 !!!html
这是本系列博客的第二篇,主要讲的是Redis基础数据结构中ZSet(有序集合)底层实现之一的Skiplist跳跃表。java
不知道那些是Redis基础数据结构的看官们,能够翻阅个人上一篇文章:面试
小白也能看懂的REDIS教学基础篇——REDIS基础数据结构算法
今天我朋友忽然找到我,说他面试被刷了。数组
我一脸吃惊,忙问到:怎么了,倒在什么题上了。数据结构
朋友说:面试官说,你说你了解Redis的基础数据结构,那我问问你,你知道什么是Skiplist跳跃表吗?讲讲它是一种什么样的数据结构。它有什么优点和缺陷,它是如何插入和删除的?dom
我:那你怎么回答的?ide
我朋友:我就说Redis不是只有五种基本数据结构 字符串(strings),列表(lists), 字典(dictht),集合(sets), 有序集合(ZSet)吗?而后人家就让我回家等通知了。post
我:...url
我朋友:怎么了,你怎么一副无语的表情。
我:哎,仍是由我来给你科普一下吧。
Skiplist 跳跃表是跳表出自 William Pugh 于1989年发表的论文《Skip Lists: A Probabilistic Alternative toBalanced Trees 》。
在论文中 William Pugh 写到;
译文大意为:
跳跃表:平衡树的几率替代方案
跳跃列表是一种能够代替平衡树的数据结构。跳跃列表使用几率平衡,而不是严格的强制平衡,所以在跳跃列表中插入和删除的算法比平衡树的算法简单得多,速度也快得多。
注:平衡树(Balance Tree) 指的是,任意节点的子树的高度差都小于等于1。常见的符合平衡树的有,B树(多路平衡搜索树)、AVL树(二叉平衡搜索树)。
看到这里,看官们是否是一头雾水?先不要急,让咱们来看看跳跃表的完整结构图。
看到这的看官是不有种想骂人的冲动?内心在想,这是个什么玩意,比平衡树还复杂。
//跳表 typedef struct zskiplist{ //头结点和尾节点的指针 struct skiplistNode *header, *tail; //表中节点的数量 unsigned long length; //表中层数最大的节点层数 int level; }; //跳表节点 typedef struct zskiplistNode{ //后退指针 struct zskiplistNode *backward; //分值 double score; //成员对象 robj *obj; //层 struct zskiplistLevel{ //前进指针 struct zskiplistNode *forward; //跨度 unsigned int span; } level[]; };
层是跳跃表节点的精髓和核心所在,跳跃表节点的level数组能够包含多个层元素。每一个层元素都包含一个指向其余节点的指针,程序能够经过这些层来快速查找其余节点。通常来讲,层数量越多,查找其余元素的速度就越快。
可是一个元素在插入时,他的层是怎么得到的呢?咱们来看下面这个方法(此方法是仿照论文中的描述,用java实现的)。
/** * 获取层级 * @param maxLevel 最大支持的层级数 * @return */ private int randomLevel(int maxLevel){ int lvl = 1; /** * 这里是关键 Math.random() > (0.5D) 等于true 的几率是 1/2 * 因此 lvl = 1 的几率是 1/2 lvl = 2 的几率 是 (1/2)*(1/2) = 1/4 * lvl = 3 的几率是 (1/2)*(1/2)*(1/2) = 1/8 从这里能够看出 lvl 越大几率越低 */ for(;Math.random() > (0.5D) && lvl < maxLevel;){ lvl += 1; } return lvl; }
从这个方法能够看出,对于每次新插入的元素,都要调用这个随机算法得到元素的层级。这里也正好对应了文章开头,论文中的话:跳跃列表使用几率平衡,而不是严格的强制平衡。
从几率上来讲,指望的目标是分配到lv 1 是50%的几率,分配到lv 2 是百分之25%的几率,分配到lv 3 是12.5% 以此类推。Redis的跳跃表共有32层,能够容纳 2^32 个元素,在Redis标准源码中
元素的晋升概率只有25%,也就是上面代码中 0.5D 这个其实应该是 0.25D。因此Redis中的跳跃表更加扁平化,层高相对不高,这就带来一个问题,层高不高的话,跨度就小,查找元素须要遍历的次数也就相应的增长了。
查找元素:
跳越表的元素查找是从header 的第 zskiplist.level(表中层数最大的节点层数) 层开始遍历的。他先会判断最高层指向的下一个元素,是不是要找的元素,若是不是,判断是否是比要找的元素小,
若是比要找的元素小,就继续查找下一个。若是比要找的元素大,就向下走一个层级,好比一开始是lv 5,若是找到的元素比要找的元素大,就降低一个层级,到lv 4 继续找。以此类推,直到找到指望的元素为止。
如今咱们要查找分数为85分的学生,查找路径以下图所示
插入元素:
建立一个新的元素节点。而后在调用 randomLevel 获取节点层级。
想要插入元素,就要先查找到全部元素中分值仅小于插入元素的分值的原数。好比要在上面的列表中插入一个分数为75分的学生二哈,就要先查找到分数仅小于75分的学生。在查找的时候还要记录下要更新的层级。如二哈这个节点拥有L4就要记录下据距他最近的L4,L3,L2,L1。
将新增节点每一个层的前进指针链接到它对应的要更新层的前进指针指向的下一个节点。而后遍历要更新的层数组,断开这些层的前进指针,并将它链接到新增的几点上。这里基本和链表是同样的更新方式。最后更新后退节点。
链接更新完成后,看看新增节点的层级是否大于跳跃表中记录的节点最大层级高度,若是大于就将跳跃表的最大层级高度更新成新节点的层级高度。
删除元素:
删除节点过程和插入相似,都须要先把这个节点找出,而后对于每一个相关节点重排一下向前向后指针,同时还要注意更新下跳跃表中记录的最大层级高度。
更新元素:
当咱们调用ZSet的zadd方法时,若是该元素不存在,就执行正常的插入过程。若是元素已经存在了,若是要更新分值,则Redis会先删除原先的元素,在插入新的元素。若是不用更新分值,Redis会直接更新节点上的元素数据(这是在5.0之后的改动,以前的是不论跟不跟新都直接先删除在插入)。
参考书籍:
《Reids设计与实现》
《Redis深度历险——核心原理与应用实践》
创做不易,若是转载请注明出处,小编在此感谢各位看官。