跳表

参考:https://blog.csdn.net/zlx_code/article/details/90581451

1、定义

跳表实际上是一种增加了前向指针的链表,是一种随机化的数据结构,实质上是可以进行二分查找有序链表;跳表在原来的有序链表上加上了多级索引,通过索引来快速查找;可以支持快速的删除、插入和查找操作。

2、跳表的理解

对于一个单链表来讲,即使链表中存储的数据是有序的,如果我们想要在其中查找某个数据,也只能从头开到尾的遍历,查询效率低,时间复杂度是O(n)。如下图所示

如何提交查找效率,如下图所示,对链表建立一级“索引”,这样查找起来方便了很多,如,每两个节点提取一个节点到上一级的,我们把抽出的那一级叫做索引或者索引层,图中的down表示down指针指向下一级节点。

           

假如我们现在查找某个节点,比如23,我们先遍历索引层,,当遍历到19的时候,发现下一个节点是25,那么23肯定在19到25这两个节点之间。我们使用down指针,下降到下一级原始链表层,继续遍历,这时候我们只需要遍历2个节点就找到23了,相比于从原始链表开始遍历,要快3个节点。

从上面可以看出,加上一层索引之后,查询效率提高了很多,那么再加上一层呢?我们在第一级索引上再加上第二级索引,建立方式和之前一样,每两个节点抽象出一个节点到第二级索引。现在查找23节点,只需要遍历6个节点就能找到,遍历的次数又减少了。

            

现在查询效率进一步提升了,现在假如节点变得越来越多,一个有64个节点,建立5个索引层,那么查询第62个节点,在没有建立索引表之前,需要遍历62个节点,建立索引表之后,只需要遍历11个节点,速度提高了很多。所以当链表的长度n比较大的时候,比如1000000,在构建索引之后,查询效率的提升会非常的明显。(摘自极客邦)

3、跳表时间复杂度分析

在一个单链表中查询的时间复杂度是O(n),现在分析一下有n个节点,有多少级索引?

按照每两个节点抽出一个节点作为上一级索引的节点,那么第一级索引节点大约是 n/2 个,第二级的索引大约是 n/4 个,以此类推,第 k 级索引的节点个数是第 k-1 级索引的节点个数的 1/2,那么第 k 级索引点的个数:n/2{^{k}}

假设索引有 h 级,最高级的索引是 2 个节点,通过上面的例子可以得到:

                                                                           frac{n}{2^h}=2

求得:   h=log_2n-1

如果包含原始链表这一层,那么整个跳表的高度是:log_2n

我们在跳表查询某个数据的时候,如果每一层都要遍历m个节点,那在跳表查询一个数据的时间复杂度是:O(m*logn),m的值是多少呢?

假设我们要找的数据是x,在第 k 级索引中,我们遍历到y节点之后,发现 x > yx < z,所以通过y的down指针,从第k级索引到第 k - 1 级索引。在第 k - 1 级索引中,y 和 z 之间只有3个节点,所以我们在第 k - 1 级索引中,最多只遍历3个节点,以此类推,每一级索引最多只需要遍历3个节点。

通过上面的分析,我们得到 m 的值是 3,所以在跳表中查找任意数据的时间复杂度是 O(logn),查找的时间复杂度和二分查找是一样的。

这种查找效率的提升,前提是建立了很多级索引。

4、跳表的空间复杂度分析

假设原始链表的长度是 N,第一级索引大约是 N/2,第二级索引大约是 N/4,以此类推,每一层减少一半,直至剩下两个点,其实就是一个等比数列,计算可以得到:frac{N}{2} + frac{N}{4} + frac{N}{8}+.... + 2 = n - 2,所以跳表的时间复杂度是 O(n),也就是说如果将n个节点的单链表构成跳表,需要额外将近n个节点的空间,如何降低跳表的存储空间?

前面的分析是使用,每两个节点抽一个节点到上级索引,如果我们使用3个节点或者5个节点:

                                                           frac{n}{3} + frac{n}{9} + frac{n}{27} + ... + 1 = frac{n-1}{2}

求和得到的节点,大约是 frac{n}{2},尽管时间复杂度还是O(n),但是存储空间减少了一半。

注意,当原始链表中存储的是很大的对象的时候,而索引节点只需要存储关键值和几个指针,并不需要存储对象,所以当对象比索引节点大很多的时候,索引节点的存储空间就可以忽略不计了。

5、高效的动态插入和删除

1、插入节点操作

跳表还支持动态的插入和删除,而且插入和删除的时间复杂度是 O(logn)。

在单链表中,一旦定位到要插入的位置,那么插入节点的时间复杂度很低 O(1),但是查找插入位置比较消耗时间。对于单链表而言,需要遍历每个节点,来找插入的位置,而对于跳表而言,查找的时间复杂度是 O(logn),所以查找某个数据应该插入的位置时间复杂度也是 O(logn)。

              

2、删除节点

如果这个节点在索引中出现,我们除了删除原始链表中的节点,还需要删除索引中的点,在单链表中删除操作是需要拿到待删除节点的前驱节点,然后再删除,如果是双向链表,则没有这个问题了。

6、跳表的动态更新

当我们不停的在跳表中增加数据的时候,如我们不更新索引,那么在极端的情况下,可能出现两个索引节点之间出现数据非常多的情况,极端情况下,退化成单链表:

                         

因此,我们需要维护索引和原始链表之间的平衡,也就是说,如果链表中的节点太多了,那么索引节点也就相应的增加了一些,避免复杂度退化,以及查找,插入和删除的操作。

7、跳表是通过随机函数来维护平衡的

当我们在跳表中插入数据的时候,我们通过选择同时将这个数据插入到部分索引层中,如何选择索引层,可以通过一个随机函数来决定这个节点插入到哪几级索引中,比如随机生成了k,那么就将这个索引加入到,第一级到第k级索引中。

              

8、总结

跳表使用的是空间换时间的思想,通过构建多级索引来提高查询效率,实现基于链表的“二分查找”,跳表是一种动态的数据结构,支持快速的查找、插入和删除操作,时间复杂度是 O(logn)。

跳表的空间复杂度是 O(n),不过跳表可以通过改变索引策略,动态的平衡执行效率和内存消耗。

原文地址:https://www.cnblogs.com/muzhongjiang/p/13895162.html