跳表的结构
对应在redis中zset 底层结构
那么如何来构建跳表?
构建原理
当每次有数据要插入时,先通过概率算法告诉我们这个元素需要插入到几级索引中,然后开始维护索引并把数据插入到原始链表中。下面开始讲解这个概率算法代码如何实现。
我们可以实现一个 randomLevel() 方法,该方法会随机生成 1~MAX_LEVEL 之间的数(MAX_LEVEL表示索引的最高层数),且该方法有 1/2 的概率返回 1、1/4 的概率返回 2、1/8的概率返回 3,以此类推。
-
randomLevel() 方法返回 1 表示当前插入的该元素不需要建索引,只需要存储数据到原始链表即可(概率 1/2)
-
randomLevel() 方法返回 2 表示当前插入的该元素需要建一级索引(概率 1/4)
-
randomLevel() 方法返回 3 表示当前插入的该元素需要建二级索引(概率 1/8)
-
randomLevel() 方法返回 4 表示当前插入的该元素需要建三级索引(概率 1/16)
-
randomLevel() 方法返回 4 表示当前插入的该元素需要建四级索引(概率 1/32)
-
... 以此类推
该 randomLevel 方法会随机生成 1~MAX_LEVEL 之间的数,且 :
-
1/2 的概率返回 1
-
1/4 的概率返回 2
-
1/8 的概率返回 3
-
... 以此类推
晋升概率 SKIPLIST_P 设置为 1/2, 代表每两个节点抽出一个节点作为上一级索引, 如果想节省空间可以适当降低该值,redis中的值为: 0.25 = 1/4,每四个节点抽出一个节点作为上一级索引。
private int randomLevel() {
int level = 1;
// 当 level < MAX_LEVEL,且随机数小于设定的晋升概率时,level + 1
while (Math.random() < SKIPLIST_P && level < MAX_LEVEL) {
level += 1;
}
return level;
}
注意: 因为 randomLevel() 方法返回值 > 1就会建索引,凡是建索引,无论几级索引必然有一级索引,所以一级索引中元素个数占原始数据个数的比率为 randomLevel() 方法返回值 > 1 的概率。那 randomLevel() 方法返回值 > 1 的概率是多少呢?
因为 randomLevel() 方法随机生成 1~MAX_LEVEL 的数字,且 randomLevel() 方法返回值 1 的概率为 1/2,则 randomLevel() 方法返回值 > 1 的概率为 1 - 1/2 = 1/2。即通过上述流程实现了一级索引中元素个数占原始数据个数的 1/2。 这样就可以满足我们想要的结果,
即:一级索引中元素个数应该占原始数据的 1/2,二级索引中元素个数占原始数据的 1/4,三级索引中元素个数占原始数据的 1/8 ,依次类推,一直到最顶层索引。
时间复杂度
跳表插入、删除、查找元素的时间复杂度跟红黑树都是一样量级的,时间复杂度都是O(logn)
空间复杂度
假如原始链表包含 n 个元素,则一级索引元素个数为 n/2、二级索引元素个数为 n/4、三级索引元素个数为 n/8 以此类推。所以,索引节点的总和是:n/2 + n/4 + n/8 + … + 8 + 4 + 2 = n-2,空间复杂度是 O(n)。
如果每三个结点抽一个结点做为索引,索引总和数就是 n/3 + n/9 + n/27 + … + 9 + 3 + 1= n/2,减少了一半。
所以, 我们可以通过较少索引数来减少空间复杂度,但是相应的肯定会造成查找效率有一定下降,我们可以根据我们的应用场景来控制这个阈值。
为什么Redis选择使用跳表而不是红黑树来实现有序集合?
zset有个查找需求特性:支持按照score范围区间查找元素
跳表:可以做到 O(logn) 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了,非常高效,但是红黑树不行。