/ Skip List

图解Skip List

如果说要学习一个简单却非常实用的“高级”数据结构,那么Skip List一定不能错过。它的提出已有二十多年[Pugh, W. (1990)],却依旧应用广泛(Redis、LevelDB等)。作为平衡树(AVL、红黑树、伸展树、树堆)的替代方案,虽然它性能不如平衡树稳定,但是在实现难度上却很有优势,可算是高性价数据结构。

Skip List是什么###

我们常用数组和链表来组织数据,对于已排序的数据,数组的查询时间复杂度可以是$\Theta(lgn)$(二分查找),插入和删除都是$\Theta(n)$。

链表提供了一种更加灵活的组织方式,插入和删除的时间复杂度是$\Theta(1)$,查询的时间复杂度是$\Theta(n)$。

平衡树是更好的方案,查询、插入、删除的时间复杂度都是$\Theta(lgn)$,但实现上比较复杂。

Skip List作为性能和实现难度的一个折中,它的查询、插入、删除等主要操作时间复杂度也都是$\Theta(lgn)$,空间复杂度是$\Theta(n)$。

一个Skip List的结构如下图,除了数据域,每个节点还包括1个或多个域用来保存后续节点的位置。

跳跃表

从结构上看,Skip List就是带有层级结构的链表(节点都是排好序的),最下面一层(level-0)是所有节点组成的一个链表,依次往上,每一层都也都是一个链表。不同的是,它们只包含一部分节点,并且越往上节点越少。仔细的话你会发现,通过增加层数,节点上可以带有更多的信息,通过这些信息可以直接访问更远的节点(这也是Skip List精髓所在),就像跳过去一样,所以取名叫Skip List(跳表)。

Skip List查询###

查询操作很简单,比如我们要找图中节点key为20的节点。

  • 我们首先获取到头节点,从头检点的最高层开始(节点中的点表示指向节点的指针),下一个节点是17, 很明显20>17所以应该在后面。继续往后结果是NULL那说明后面没有要找的节点了。
  • 跳到下一层继续往后是2520<25说明后面也没有我们要找的节点了。
  • 再跳到下一层,往后就找到我们要的节点了。如果到最下面一层还找不到,那这个节点就肯定不在表中了(因为最低层包含所有节点)。

跳跃表查找

对比一下,如果只有一层(图中level-0),我们要找20这个节点是需要5次才能找到,但是加上层级,我们有了更远节点的信息,利用更远节点的信息缩小查找范围,这里例子中我们只需要2次就找到了。

Skip List插入###

插入操作稍微复杂, 首先我们要找到插入位置,怎么找我们刚才已经描述过了。如下图所示,插入key为10的节点,插入点应该是节点9和节点12之间(紫色的线表示要更新的指向)。然后是插入节点10,其实就是链表的逐层插入。
跳跃表插入

这里的需要注意是,逐层插入需要知道节点在每一层的位置,如在level-2中,节点10前面应该是头结点,而后面应该是节点17。 因为查询操作得到的只是最后位置,所以通常需要一个临时的空间来记录这些信息。如果节点的高度超过超过了Skip List的最大层数,那么Skip List的层数相应的需要升高。如节点10的高度是4的话,根据Skip List的结构特点,那么层数需要提高到level-3。

节点高度与Skip List的最大层数####

节点的高度可以理解为需要多少域来保存后续节点的信息,Skip List的层数跟结构中最高节点的高度相等。理想的SkipList结构(如图一)是第一层有所有的节点,第二层只有1/2的节点,且是均匀间隔的,第三层是1/4的节点,且是均匀间隔的...,那么理想的层数是$lgn$。

每一次插入一个新节点时,最好的做法就是根据当前表的结构得到一个合适的高度,插入后可以让Skip List的尽量的接近理想的结构,但是实现上这会非常的复杂。

Pugh论文中提出的方法是根据概率随机为新节点生成一个高度,具体的算法如下:

  • 给定一个概率$p$, 产生一个$[0,1)$ 之间的随机数
  • 如果这个随机数小于$p$,则高度加$1$
  • 重复以上动作,直到随机数大于概率$p$

虽然随机生成的高度会打破理想的结构,Pugh在论文中证明,这种结构依然有非常高概率可以使得时间复杂度为$\Theta(lgn)$。

通常我们还会约束Skip List的最大层数,公式:$maxLevel=log_{1/p}n$,其中n表示节点总数。 根据Pugh论文中的结论,p为1/2或者1/4时,整体性能会比较好。(当p=1/2时,确定节点高度有的地方称为抛硬币的方法)。

Skip List删除###

删除操作跟插入操作类似。 首先找到我们要删除节点的位置,在查找时使用临时空间记录节点在每一级的位置。 接着就是逐层的链表删除操作。 最后记住要释放空间。 删除节点之后,如果最高层没有节点存在,那Skip List的层数相应的需要降下来。 下图表示节点9的删除操作。
跳跃表删除

小结###

如果对链表的实现很熟悉,实现Skip List也很容易。本文没有具体代码,一方面我不想文章篇幅因为代码变得很大,另一方面网上已经有很多实现。关于复杂度分析,当p=1/2时或者理想结构下,很好分析。推广到一般情况,那么分析也会比较复杂,需要比较高的数学功底,有兴趣可以看看Pugh在论文,另外MIT的算法公开课教授对Skip List的讲解也非常棒。

堃田

堃田

不要畏惧眼前的困难,克服它们就能获得成长和机会。

Read More