链表中的跳表小结

链表之所 以访问中间节点的效率低,就是因为每个节点只存储了下一个节点的指针,要沿着这个指针
遍历每个后续节点才能到达中间节点。那如果我们在节点上增加一个指针,指向更远的节
点,比如说跳过后一个节点,直接指向后面第二个节点,那么沿着这个指针遍历,是不是遍
历速度就翻倍了呢?
同理,如果我们能增加更多的指针,提供不同步长的遍历能力,比如一次跳过 4 个节点,
甚至一半的节点,那我们是不是就可以更快速地访问到中间节点了呢?
这当然是可以实现的。我们可以为链表的某些节点增加更多的指针。这些指针都指向不同距
离的后续节点。这样一来,链表就具备了更高效的检索能力。这样的数据结构就是 跳表
(Skip List)。
一个理想的跳表,就是从链表头开始,用多个不同的步长,每隔 2^n 个节点做一次直接链
接(n 取值为 0,1,2……)。跳表中的每个节点都拥有多个不同步长的指针,我们可以在
每个节点里,用一个数组 next 来记录这些指针。next 数组的大小就是这个节点的层数,
next[0]就是第 0 层的步长为 1 的指针,next[1]就是第 1 层的步长为 2 的指针,next[2]就
是第 2 层的步长为 4 的指针,依此类推。你会发现,不同步长的指针,在链表中的分布是
非常均匀的,这使得整个链表具有非常平衡的检索结构。


 
举个例子,当我们要检索 k=a 6 时,从第一个节点 a 1 开始,用最大步长的指针开始遍历,直
接就可以访问到中间节点 a 5 。但是,如果沿着这个最大步长指针继续访问下去,下一个节
点是大于 k 的 a 9 ,这说明 k 在 a 5 和 a 9 之间。那么,我们就在 a 5 和 a 9 之间,用小一个级别
的步长继续查询。这时候,a 5 的下一个元素是 a 7 ,a 7 依然大于 k 的值,因此,我们会继续
在 a 5 和 a 7 之间,用再小一个级别的步长查找,这样就找到 a 6 了。这个过程其实就是二分查
找。时间代价是 O(log n)。
跳表的检索空间平衡方案
不知道你有没有注意到,我在前面强调了一个词,那就是“理想的跳表”。为什么要叫
它“理想”的跳表呢?难道在实际情况下,跳表不是这样实现的吗?的确不是。当我们要在
跳表中插入元素时,节点之间的间隔距离就被改变了。如果要保证理想链表的每隔 2^n 个
节点做一次链接的特性,我们就需要重新修改许多节点的后续指针,这会带来很大的开销。
所以,在实际情况下,我们会在检索性能和修改指针代价之间做一个权衡。为了保证检索性
能,我们不需要保证跳表是一个“理想”的平衡状态,只需要保证它在大概率上是平衡的就
可以了。因此,当新节点插入时,我们不去修改已有的全部指针,而是仅针对新加入的节点
为它建立相应的各级别的跳表指针。具体的操作过程,我们一起来看看。
首先,我们需要确认新加入的节点需要具有几层的指针。我们通过随机函数来生成层数,比
如说,我们可以写一个函数 RandomLevel(),以 (1/2)^n 的概率决定是否生成第 n 层。这
样,通过简单的随机生成指针层数的方式,我们就可以保证指针的分布,在大概率上是平衡
的。
在确认了新节点的层数 n 以后,接下来,我们需要将新节点和前后的节点连接起来,也就
是为每一层的指针建立前后连接关系。其实每一层的指针链接,你都可以看作是一个独立的
单链表的修改,因此我们只需要用单链表插入节点的方式完成指针连接即可。
这么说,可能你理解起来不是很直观,接下来,我通过一个具体的例子进一步给你解释一
下。
我们要在一个最高有 3 层指针的跳表中插入一个新元素 k,这个跳表的结构如下图所示。
 
假设我们通过跳表的检索已经确认了,k 应该插入到 a 6 和 a 7 两个节点之间。那接下来,我
们要先为新节点随机生成一个层数。假设生成的层数为 2,那我们就要修改第 0 层和第 1
层的指针关系。对于第 0 层的链表,k 需要插入到 a 6 和 a 7 之间,我们只需要修改 a 6 和 a 7
的第 0 层指针;对于第 1 层的链表,k 需要插入到 a 5 和 a 7 之间,我们只需要修改 a 5 和 a 7
的第 1 层指针。这样,我们就完成了将 k 插入到跳表中的动作。
通过这样一种方式,我们可以大大减少修改指针的代价。当然,由于新加入节点的层数是随
机生成的,因此在节点数目较少的情况下,如果指针分布的不合理,检索性能依然可能不
高。但是当节点数较多的时候,指针会趋向均匀分布,查找空间会比较平衡,检索性能会趋
向于理想跳表的检索效率,接近 O(log n)
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页