跳表(skiplist)及redis实现

解决的问题

解决插入和查找的问题,比红黑树更容易实现。

论文原文

Skip Lists: A Probabilistic Alternative to
Balanced Trees

基本结构

跳表是一个基于概率构造的数据结构。
跳表由层构成,第i层的元素在第i+1层出现的概率是一个固定值:p(通常是1/21/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[]数组,该数组的最大大小是32x->level[i].forward就是x在第i层的下一个节点。

  1. 使用数组,这里隐藏了上下层之间相连的事实。也就是说,如果一个节点在第i层存在,则一定也在第i-1层存在;
  2. 此外,后继节点在第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

总体步骤分为两步:

  1. 最高层开始往下查找,在每次一层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()生成的值的层,才会被更新。

  1. 生成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)
  • ZRANGEZREVRANGE操作可以以线性的方式实现,相比之下缓存局部性和其他平衡树没有区别
  • 更加容易实现和调试,ZRANK能够以O(log(n))复杂度实现,只需要对原来的代码做一定的修改即可

关于MAXLEVEL参数的设置,还有相关的PR:Set ZSKIPLIST_MAXLEVEL to 32 #6818 (2020).

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值