Redis跳表的时间发杂度和空间复杂度的分析

Redis跳表的时间发杂度和空间复杂度的分析:

  • 首先,每个节点肯定都有第1层指针(每个节点都在第1层链表里)。
  • 如果一个节点有第i层(i>=1)指针(即节点已经在第1层到第i层链表中),那么它有第(i+1)层指针的概率为p。
  • 节点最大的层数不允许超过一个最大值,记为MaxLevel。

这个计算随机层数的伪码如下所示:

randomLevel()
    level := 1
    // random()返回一个[0...1)的随机数
    while random() < p and level < MaxLevel do level := level + 1 return level 

randomLevel()的伪码中包含两个参数,一个是p,一个是MaxLevel。在Redis的skiplist实现中,这两个参数的取值为:

p = 1/4
MaxLevel = 32

skiplist的算法性能分析

在这一部分,我们来简单分析一下skiplist的时间复杂度和空间复杂度,以便对于skiplist的性能有一个直观的了解。如果你不是特别偏执于算法的性能分析,那么可以暂时跳过这一小节的内容。

我们先来计算一下每个节点所包含的平均指针数目(概率期望)。节点包含的指针数目,相当于这个算法在空间上的额外开销(overhead),可以用来度量空间复杂度。

根据前面randomLevel()的伪码,我们很容易看出,产生越高的节点层数,概率越低。定量的分析如下:

  • 节点层数至少为1。而大于1的节点层数,满足一个概率分布。
  • 节点层数恰好等于1的概率为1-p。
  • 节点层数大于等于2的概率为p,而节点层数恰好等于2的概率为p(1-p)。
  • 节点层数大于等于3的概率为p 2 ,而节点层数恰好等于3的概率为p 2 (1-p)。
  • 节点层数大于等于4的概率为p 3 ,而节点层数恰好等于4的概率为p 3 (1-p)。

因此,一个节点的平均层数(也即包含的平均指针数目),计算如下:

img

现在很容易计算出:

  • 当p=1/2时,每个节点所包含的平均指针数目为2;
  • 当p=1/4时,每个节点所包含的平均指针数目为1.33。这也是Redis里的skiplist实现在空间上的开销。

接下来,为了分析时间复杂度,我们计算一下skiplist的平均查找长度。查找长度指的是查找路径上跨越的跳数,而查找过程中的比较次数就等于查找长度加1。以前面图中标出的查找23的查找路径为例,从左上角的头结点开始,一直到结点22,查找长度为6。

为了计算查找长度,这里我们需要利用一点小技巧。我们注意到,每个节点插入的时候,它的层数是由随机函数randomLevel()计算出来的,而且随机的计算不依赖于其它节点,每次插入过程都是完全独立的。所以,从统计上来说,一个skiplist结构的形成与节点的插入顺序无关。

这样的话,为了计算查找长度,我们可以将查找过程倒过来看,从右下方第1层上最后到达的那个节点开始,沿着查找路径向左向上回溯,类似于爬楼梯的过程。我们假设当回溯到某个节点的时候,它才被插入,这虽然相当于改变了节点的插入顺序,但从统计上不影响整个skiplist的形成结构。

现在假设我们从一个层数为i的节点x出发,需要向左向上攀爬k层。这时我们有两种可能:

  • 如果节点x有第(i+1)层指针,那么我们需要向上走。这种情况概率为p。
  • 如果节点x没有第(i+1)层指针,那么我们需要向左走。这种情况概率为(1-p)。

这两种情形如下图所示:

Redis跳表的时间发杂度和空间复杂度的分析_第1张图片

用C(k)表示向上攀爬k个层级所需要走过的平均查找路径长度(概率期望),那么:

C(0)=0
C(k)=(1-p)×(上图中情况b的查找长度) +(上图中情况c的查找长度)

代入,得到一个差分方程并化简:

C(k)=(1-p)(C(k)+1) + p(C(k-1)+1)
C(k)=1/p+C(k-1)
C(k)=k/p

这个结果的意思是,我们每爬升1个层级,需要在查找路径上走1/p步。而我们总共需要攀爬的层级数等于整个skiplist的总层数-1。

那么接下来我们需要分析一下当skiplist中有n个节点的时候,它的总层数的概率均值是多少。这个问题直观上比较好理解。根据节点的层数随机算法,容易得出:

  • 第1层链表固定有n个节点;
  • 第2层链表平均有n*p个节点;
  • 第3层链表平均有n*p 2 个节点;

所以,从第1层到最高层,各层链表的平均节点数是一个指数递减的等比数列。容易推算出,总层数的均值为log 1/p n,而最高层的平均节点数为1/p。

综上,粗略来计算的话,平均查找长度约等于:

  • C(log 1/p n-1)=(log 1/p n-1)/p

即,平均时间复杂度为O(log n)。

当然,这里的时间复杂度分析还是比较粗略的。比如,沿着查找路径向左向上回溯的时候,可能先到达左侧头结点,然后沿头结点一路向上;还可能先到达最高层的节点,然后沿着最高层链表一路向左。但这些细节不影响平均时间复杂度的最后结果。另外,这里给出的时间复杂度只是一个概率平均值,但实际上计算一个精细的概率分布也是有可能的。详情还请参见William Pugh的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》。

具体请看下面:

https://blog.csdn.net/u010412301/article/details/64923131

你可能感兴趣的:(Redis)