什么是跳表?
链表+ 多级索引
为什么需要跳表?
对于一个单链表
来讲,即便链表中存储的数据是有序的,如果我们要想在其中查找某个数据,也只能从头到尾遍历链表
。这样查找效率就会很低,时间复杂度会很高,是 O(n)
。
那怎么来提高查找效率呢?如果像图中那样,对链表建立一级“索引”
,查找起来是不是就会更快一些呢?每两个结点提取一个结点到上一级,我们把抽出来的那一级叫做索引或索引层
。你可以看我画的图。图中的 down 表示 down 指针
,指向下一级结点。
如果我们现在要查找某个结点,比如 16。我们可以先在索引层遍历,当遍历到索引层中值为 13 的结点时,我们发现下一个结点是 17,那要查找的结点 16 肯定就在这两个结点之间。然后我们通过索引层结点的 down 指针
,下降到原始链表这一层,继续遍历。这个时候,我们只需要再遍历 2 个结点,就可以找到值等于 16 的这个结点了。这样,原来如果要查找 16,需要遍历 10 个结点,现在只需要遍历 7 个结点。
从这个例子里,我们看出,加来一层索引之后,查找一个结点需要遍历的结点个数减少了,也就是说查找效率提高
了。
那如果我们再加一级索引呢?效率会不会提升更多呢?跟前面建立第一级索引的方式相似,我们在第一级索引的基础之上,每两个结点就抽出一个结点到第二级索引
。现在我们再来查找 16,只需要遍历 6 个结点了,需要遍历的结点数量又减少了。
我举的例子数据量不大,所以即便加了两级索引
,查找效率的提升也并不明显。为了让你能真切地感受索引提升查询效率。我画了一个包含 64 个结点的链表,按照前面讲的这种思路,建立了五级索引。
从图中我们可以看出,原来没有索引的时候,查找 62 需要遍历 62 个结点,现在只需要遍历 11 个结点,速度是不是提高了很多?所以,当链表的长度 n 比较大时,比如 1000、10000 的时候,在构建索引之后,查找效率的提升就会非常明显。
前面讲的这种链表加多级索引的结构,就是跳表
。我通过例子给你展示了跳表是如何减少查询次数的,现在你应该比较清晰地知道,跳表确实是可以提高查询效率
的。接下来,我会定量地分析一下,用跳表查询到底有多快。
跳表查找的时间复杂度
前面我讲过,算法的执行效率可以通过时间复杂度
来度量,这里依旧可以用。我们知道,在一个单链表中查询某个数据的时间复杂度是 O(n)
。那在一个具有多级索引的跳表中,查询某个数据的时间复杂度是多少呢?这个时间复杂度的分析方法比较难想到。我把问题分解一下,先来看这样一个问题,如果链表里有 n 个结点,会有多少级索引呢?
按照我们刚才讲的,每两个结点会抽出一个结点作为上一级索引的结点,那第一级索引的结点个数大约就是 n/2
,第二级索引的结点个数大约就是 n/4
,第三级索引的结点个数大约就是 n/8
,依次类推,也就是说,第 k 级索引的结点个数是第 k-1 级索引的结点个数的 1/2,那第 k级索引结点的个数就是 n/(2^k)
。
假设索引有 h 级,最高级的索引有 2 个结点。通过上面的公式,我们可以得到 n/(2^h)=2
,从而求得 h=log2n-1。如果包含原始链表这一层,整个跳表的高度就是 log2n。我们在跳表中查询某个数据的时候,如果每一层都要遍历 m 个结点,那在跳表中查询一个数据的时间复杂度就是 O(m*logn)
。
那这个 m 的值是多少呢?按照前面这种索引结构,我们每一级索引都最多只需要遍历 3 个结点,也就是说 m=3,为什么是 3 呢?我来解释一下。
假设我们要查找的数据是 x,在第 k 级索引中,我们遍历到 y 结点之后,发现 x 大于 y,小于后面的结点 z,所以我们通过 y 的 down 指针,从第 k 级索引下降到第 k-1 级索引。在第 k-1 级索引中,y 和 z 之间只有 3 个结点(包含 y 和 z),所以,我们在 K-1 级索引中最多只需要遍历 3 个结点,依次类推,每一级索引都最多只需要遍历 3 个结点。
通过上面的分析,我们得到 m=3,所以在跳表中查询任意数据的时间复杂度就是 O(logn)
。这个查找的时间复杂度跟二分查找是一样的。换句话说,我们其实是基于单链表实现了二分查找
,是不是很神奇?不过,天下没有免费的午餐,这种查询效率的提升,前提是建立了很多级索引,也就是空间换时间
的设计思路。
总结
- 跳表= 链表+多级索引
- 跳表查找的时间复杂度为
O(log(n))
,而单链表查找的时间复杂度为O(n) - 跳表是为了解决单链表查找
时间复杂度高
的问题 - 跳表反映了
空间换时间
的设计思路