解决的问题
解决插入和查找的问题,比红黑树更容易实现。
论文原文
Skip Lists: A Probabilistic Alternative to
Balanced Trees
基本结构
跳表是一个基于概率构造的数据结构。
跳表由层构成,第i
层的元素在第i+1
层出现的概率是一个固定值:p
(通常是1/2
或1/4
).
跳表的最底层是一个排序的链表。
[4] 30 NIL
[3] 30 50 NIL
[2] 30 50 70 NIL
[1] 30 40 45 50 60 70 80 90 NIL
redis zset源码
https://github.com/redis/redis/blob/unstable/src/t_zset.c
跳表数据定义: MAX_LEVEL=32
,p=1/4
#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^64 elements */
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
ZSKIPLIST_MAXLEVEL = log[1/p](n) = log[1/0.25](2^64) = 32
,它表示在p=0.25
的情况下,允许容纳至多2^64
个元素的跳表的最大层级为32
.
结构定义:
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
所对应的存储结构如下:https://app.diagrams.net/#Hxhd2015%2Fbog%2Fmain%2Fskip_list
每个zskiplistNode
包含一个level[]
数组,该数组的最大大小是32
。x->level[i].forward
就是x
在第i
层的下一个节点。
- 使用数组,这里隐藏了上下层之间相连的事实。也就是说,如果一个节点在第
i
层存在,则一定也在第i-1
层存在; - 此外,后继节点在第
i
层的有效性,完全由前驱节点的forward
有效性决定。亦即,节点n
存在于第i
层,当且仅当:
// CodeQL表达式
isEffective(Node n,int i):
exists(Node x| isEffective(x,i) and x.level[i].forward = n)
search
search(lst,e):
x=lst.header
for i = lst.lvl - 1 downto 0:
while x.level[i].forward && x.level[i].forward.key < e:
x = x.level[i].forward
if x.key == e:
return true
return false
insert
总体步骤分为两步:
- 从最高层开始往下查找,在每次一层
i
中找到最后一个小于e
的元素x
,将其赋值给update[i]=x
insert(lst,e):
update := make([]Node,MAX_LEVEL)
x:=lst.header
for i = lst.lvl - 1 downto 0:
while x.level[i].forward && x.level[i].forward.key < e:
x = x.level[i].forward
update[i] = x
...
循环结束后,update
中保存了每一层应当被更新的节点。但是他们并不一定会被更新,只有小于等于randomLevel()
生成的值的层,才会被更新。
- 生成
randomLevel
,并执行更新动作:插入节点
insert(lst,e):
...
lvl := randomLevel() // 假设 lvl<=lst.level
nd = createNode(lvl,e)
for i=0 to lvl-1:
nd->level[i].forward = update[i]->level[i].forward
update[i]->level[i].forward = nd
lst.length++
randomLevel
实现:
randomLevel():
i=1
// random() 随机返回[0,1)中的值
while random()<p and i<MAX_LEVEL:
i++
return i
层级越高的返回的概率越低,是二项分布
的。返回第一层的概率是p
, 返回第二层的概率是(1-p)p
,返回第i
层的概率是(1-p)^(i-1)*p
.
delete
实现参考(go)
https://github.com/xhd2015/bog/blob/main/code/skiplist/skiplist.go
find
的实现需要稍微注意:因为定位到的元素是每一层里最后一个小于key
的元素,所以最后应当判断x.forward[0].key
而不是x.key
.
题目:请找出下面代码的实现错误
func (c *list) insert(key int) {
var update [MAX_LEVEL]*node
x := c.header
for i := MAX_LEVEL - 1; i >= 0; i-- {
// x at the rightmost element that is smaller than `key`
if x.forward[i] != nil && x.forward[i].key < key {
x = x.forward[i]
}
update[i] = x // has x.forward[i].key >= key
}
lvl := randomLevel()
nd := newNode()
nd.key = key
for i := 0; i < lvl; i++ {
nd.forward[i] = update[i].forward[i]
update[i].forward[i] = nd
}
}
redis为什么使用跳表而不是红黑树?
从时间复杂度的角度来分析,两者无论是在元素查找还是区间查找上,都具有相同的复杂度。
Stackoverflow上提供的链接给出了作者原来的回答:
- 可以通过调整参数来实现空间和查找时间的平衡
比如:Change skip list P value to 1/e, which mproves search times #3889 (2017) ZRANGE
和ZREVRANGE
操作可以以线性的方式实现,相比之下缓存局部性和其他平衡树没有区别- 更加容易实现和调试,
ZRANK
能够以O(log(n))
复杂度实现,只需要对原来的代码做一定的修改即可
关于MAXLEVEL
参数的设置,还有相关的PR:Set ZSKIPLIST_MAXLEVEL to 32 #6818 (2020).