一个lua版的zset数据结构实现

来源:互联网 发布:沥青库存数据 编辑:程序博客网 时间:2024/05/19 17:58

redis里的zset是一种有序集合,从逻辑上,可以理解为在集合的基础上,为每一个成员增加了分数字段,分数是一种浮点数值,并可以相同,它们按序排列起来。它能对分数在增,删,查找上都能提供对数时间复杂度的操作。

redis里的zset是利用一个skiplist和一个dict实现的,其中关键数据结构就是skiplist,跳跃表。

skiplist的原理和基本实现网上有很多,不再啰嗦。基于zset的需求,会有稍微的改进。

1,结构定义

typedef struct skiplistnode {    double score;    int member; /* (1) */    struct skiplistnode *backward; /* (2) */    struct skiplistforward {        struct skiplistnode *ptr;        unsigned long span; /* (3) */    } forward[];} skiplistnode;typedef struct skiplist {    int level;    unsigned long length;    struct skiplistnode *header, *tail;} skiplist;

基本参考了redis的定义,其中member为什么是int(1),是用来放luaL_ref的ref值,所有放在容器里的数据都会先引用,然后记在这个C结构里,所以它可以记任何lua类型:函数,对象等。那个backward(2),是用反向遍历结点,比如zrevrange*这类操作。那个span的含义是(3),当前那一层的下一跳ptr距离本结构的距离,这是为了实现rank计算排名等相关操作的,为什么要这样定义,这样可以在只有局部结点变化时,需要修改的span值比较少,而且也比较容易实现各种计数操作。

2,为什么skiplist有平均很好的性能

具体有严格的证明,我只写下我自己的理解。本质上它就是一个list,只不过每个结点有一个层级level,任何结构的层级都是随机生成,像这样:

intsl_randomlevel() {    int level = 1;    while (random() * 2 < RAND_MAX)        level++;    return level > MAX_SKIP_LEVEL ? MAX_SKIP_LEVEL : level;}

那个随机函数的比较意思是,概率为p(比如1/2)的随机事件是否发生,如果发生level++,那么如果p=1/2,那level的取值和机率像这样:

  • level = 1 , p=12
  • level = 2 , p=122
  • level = n , p=12n

层级越高的结点成指数减少,一个level=n的结点,会有1至n的forward指针,指向下一个较大分数的结点。这样查找结点时,从最高层级的header开始,往右查找,当右结点的分值或者排名不符合条件时,层级减少,这样一直循环,直到0层的右结点不符合条件,查找就完成了,每一层往下跳的结点就是这次查找的关键结点,每层一个,它表示当前查找的值在每一层上所处的位置,查找,修改和删除都基于这个基本过程。如果最高层级是n,那从平均概率上会有O(2^n)个结点,本层的查找平均也会减少一半结点数。

3,字典

实现zset还需要一个dict,用来保存当前member的分值等。这样能过member修改分数的过程就是:从dict查找当前分数,通过score,member删除当前结点,添加新的score,member。

4,枚举器

zset还实现了一系列枚举器:zset:forward(), zset:backward(), zset:range()等,这样有个小问题,在枚举遍历的过程当中如果删除当前结点,要保证不能破坏当前枚举,不能crash,也不能漏元素。这里用了个小技巧,每次当前结点返回前,把下一个结点计算好,下次进来时直接先赋值下一个结点,这样当前结点,作为list进行了删除是不会影响下一个结点的。

开源代码:(待上传)

TODO

redis的zset对于相同分数的结点之间的顺序是基于member的字典序,因为member是redis字符串,所以这样实现比较巧妙,在lua环境下member是任何数据类型,因此没有这一系列操作。除此之外,还有几个批量删除的操作也未实现,zremrange*,单个结点的删除是O(logn),如果删除m个结点,用原有删除是O(mlogn),而这几个批量删除仍然保持O(logn+m)。实现思路很明显,类似查询过程,以后完善代码。

0 0
原创粉丝点击