摘要:
跳表是基于链表的数据结构,查找、插入及删除数据时间复杂度都为 O ( log n ) O(\log{n}) O(logn),空间复杂度为 O ( n ) O(n) O(n),也是利用了空间换时间的概念提高了链表的执行效率。
基于链表的二分查找
在之前的文章有提到过二分查找基于链表实现时会导致算法效率严重下降,但 O ( log n ) O(\log{n}) O(logn) 的执行效率实在诱人,难道链表没有办法在不降低二分查找执行效率的基础上实现它吗?链表肯定有相应的解决方案,但需要使用基于链表扩展的数据结构「跳表」(Skip list)。
跳表的英文名「Skip list」中的 list 表示它是基于链表的,那在链表的基础上是如何实现 Skip 的呢?链表随机访问某个结点效率低的原因是需要遍历目标结点之前的所有结点,如果我们将链表中的两个结点归为一个区域,查找结点时先查找所在区域,再在区域的小范围数据中查找目标结点,效率肯定提升很多,两个结点抽取形成的区域当然也使用链表实现。举个例子,如果当前链表有 10 个结点,此时查找第 8 个结点需要遍历 8 次,我们将每两个结点抽取成一个结点形成区域,区域的结点个数就是 5 个,查找原链表上第 6 个结点可以先在区域链表上查找,对应的是第 4 个结点的区域,再在此区域的原链表上查找,总共只用查找 5 次结点。
既然对链表抽取区域后可以提高执行效率,如果对区域链表进行同样的操作执行效率会发生怎样的变化?接着上面的例子,对第二级链表进行两个结点抽取成一个结点形成区域,形成的链表有 3 个结点,依然是查找第一级链表的第 8 个结点,这次只需要查找 4 次,所以执行效率是提升的。
其实抽取结点形成的区域链表和索引是类似的,也可以认为抽取出的链表都是原链表的索引链表。之前举的例子数据量不大,能够抽取的索引级数也较少,效果不太明显,如果将数据量扩大到 32 个,原始链表查找第 31 个结点时需要遍历 31 次,我们将索引的级数增加,直至索引链表的结点个数为 2,此时查找原链表上第 31 个结点只需要查找 5 次,可以看出查找的效率在提取索引后得到了极大的提升。
跳表的时间复杂度
从上面的例子中可以看出跳表的执行效率极高,但具体多高就需要分析一下跳表的时间复杂度了。我们从两个结点提取一个结点作为索引链表的结点,索引链表的结点数是前一级链表结点数的 1 2 \frac{1}{2} 21,假设有一个链表结点个数为 n,它的第一级至第 k 级索引链表的结点个数依次为 n 2 , n 4 , n 8 , . . . , n 2 k \frac{n}{2},\frac{n}{4},\frac{n}{8},...,\frac{n}{2^k} 2n,4n,8n,...,2kn,第 k 级索引链表为 2 个结点,所以满足如下数学关系。
n 2 k = 2 \frac{n}{2^k}=2 2kn=2
2 k + 1 = n 2^{k+1}=n 2k+1=n
k = log 2 n − 1 k=\log_2{n}-1 k=log