首页 > 其他分享 >【godis】skiplist

【godis】skiplist

时间:2023-01-28 16:44:16浏览次数:62  
标签:level update rank skiplist forward godis zsl 节点

skiplist

前言:在看代码时看到 ZSKIPLIST_MAXLEVEL = 32,当时并不了解 ZSKIPLIST_P 的作用,想着用 2 分法不应该层数是 64 吗?书上和他人的代码都是基于 5.0 的(5.0 上是 64),于是好奇后面又为什么改了。于是查了一些资料做下记录

为什么 ZSKIPLIST_MAXLEVEL = 32 ?

https://stackoverflow.com/questions/60017681/is-zskiplist-maxlevel64-enough-for-264-elements

ZSKIPLIST_MAXLEVEL 和 ZSKIPLIST_P 相关:

  1. 将元素个数上限设置在 264 已经是一个很大的值了,如果再大也存不下了,因为用来表示总数的 length 的类型 unsigned long 在 64 位机器上也只有 8 个字节
  2. 平均每两个节点升一层,那么需要 64 层;而如果平均每四个节点升一层,只需要 32 层,此时相对于 n 层,有 1/4 的节点有 n+1 层,因此 ZSKIPLIST_P=0.25。
    • 至于为什么取 1/4,下方提供的论文中作者在 Choosing p 中作了说明

跳表原理

https://homepage.cs.uiowa.edu/~ghosh/skip.pdf
image

  • d:

    • 优点:
      相对于 c: d 的每一层的指针都指向距离它 2level 位置的节点, 因此将搜索的时间复杂度降到了 O(log2n)
    • 缺点:
      增删改节点时,需要时刻考虑修改跳表中其他节点的层数,增大修改的难度
  • e:

    • 优点:
      从 d 中可以找到规律,由于层数只会在索引为 2 的倍数的节点上增加,因此索引为单数的节点的层数只有一层,共有 50% 个这样的节点,层数为 2 的节点数量为 25%, 以此类推……因此,不妨将所有节点的层数打乱,但仍然保持这样的比例分配,并且无法修改节点的层数。这样,增删改节点时,都不会影响其他节点的层数。
    • 缺点:
      就像抛硬币一样,尽管理论上正反面的概率都是 50%,但那也是在大量实验后才会逐渐接近这个理论值。如果节点比较少,计算得到的层数都很低或都很高,那么就变成了一条链表,时间复杂度降低到 O(n)。因此,通过该方式虽然可以减少代码复杂度,但是能否准确的达到预期,这是随机的。

随机层高

实现 e 的关键部分,减小代码实现难度。
假设 d 中的底数为 3,第二层在第一层的基础上每个加一层;第三层又是在第二层的基础上每个加一层……最终,只有一层的节点比例为 \(\frac{2}{3}\), 第二层的比例为 \((1-\frac{2}{3})\cdot\frac{2}{3}\)
因此以一般情况 p 代替 \(\frac{2}{3}\),可以得到

\[P(level_{i}) = (1-p)^{i-1}p \]

random level
func randomLevel() int {
  newLevel := 1
  for rand.Float64() < p { // 这里的 p 为非该层的概率
    newLevel++
  }
  return newLevel
}

以下用 insert 方法来体现 random_level 的优势:

因为用到了随机高度,因此在插入新节点时无需改动其他节点的高度。但是需要保存所有层级比新节点低的原本可以指向新节点之后节点forward,可以在降低层级时保存每个 forward
【注】
在论文中,如果插入了含有已存在的 search key 的 element,则会替换旧的 element

func (zsl *zskiplist) Insert(score float64, ele string) *zskiplistNode {
  var (
    update  [ZSKIPLIST_MAXLEVEL]*zskiplistNode
    rank    [ZSKIPLIST_MAXLEVEL]uint64
    x       *zskiplistNode
  )

  x = zsl.header
  for i := zsl.level - 1; i >= 0; i-- {
    if i== zsl.level - 1 {
      rank[i] = 0
    } else {
      rank[i] = rank[i+1]
    }
    for x.level[i].forward != nil &&
    (x.level[i].forward.score < score ||
    (x.level[i].forward.score == score && x.level[i].forward.ele < ele)) {
      x = x.level[i].forward
      rank[i] += x.level[i].span
    }
    update[i] = x
  }
}

x = x.level[0].forward
if x != nil && x.score == score {
  x.ele = ele
  return x
}

level := randomLevel()
if zsl.level < level {
  for i := zsl.level; i < level; i++ {
    rank[i] = 0
    update[i] = zsl.header
    update[i].level[i].span = zsl.length
  }
  zsl.level = level
}

x = zslCreateNode(level, score, ele)
for i := 0; i < zsl.level; i++ {
  x.level[i].forward = update[i].level[i].forward
  update[i].level[i].forward = x

  x.level[i].span = update[i].level[i].span - (rank[0] - rank[i])
  update[i].level[i].span = rank[0] - rank[i] + 1
}

for i := level; i < zsl.level; i++ {
  update[i].level[i].span++
}

if update[0] == zsl.header {
  x.backward = nil
} else {
  x.backward = update[0]
}

if x.level[0].forward == nil {
  zsl.tail = x
} else {
  x.level[0].forward.backward = x
}

zsl.length++
return x

标签:level,update,rank,skiplist,forward,godis,zsl,节点
From: https://www.cnblogs.com/HelloEricy/p/17060873.html

相关文章

  • ConcurrentSkipListMap以及跳查表简单介绍
    ConcurrentSkipListMap是一个有序的hashMap集合,看例子  底层原理是跳查表 当节点删除和节点添加同时操作就会报错,这是因为链表中删除数据是通过头节点的移动来操作......
  • redis底层数据结构之跳表(skiplist)
    跳表(跳跃表,skiplist)跳跃表(skiplist)是用于有序元素序列快速搜索查找的数据结构,跳表是一个随机化的数据结构,实质是一种可以进行二分查找的、具有层次结构的有序链表......
  • Redis原理 - 对象的数据结构(SDS、Inset、Dict、ZipList、QuickList、SkipList、RedisO
    Redis数据结构1.SDSRedis是用C语言写的,但是对于Redis的字符串,却不是C语言中的字符串(即以空字符’\0’结尾的字符数组),它是自己构建了一种名为简单动态字符串(sim......
  • LevelDB源码剖析(3) Skiplist跳表
    1.背景什么是跳表?跳表是SortedMap的一种具体实现,是一种概率性的数据结构。跳表拥有SortedMap的所有功能,定位和红黑树类似,其和红黑树的区别在于优点:跳表的实现更加简单......
  • JAVA并发容器-ConcurrentSkipListMap,ConcurrentSkipListSet
    ConcurrentSkipListMap其实是TreeMap的并发版本。TreeMap使用的是红黑树,并且按照key的顺序排序(自然顺序、自定义顺序),但是他是非线程安全的,如果在并发环境下,建议使用Concurre......