谁说有序链表不能进行二分查找,只是须要进化而已?!

前言

本文收录于专辑:http://dwz.win/HjK,点击解锁更多数据结构与算法的知识。java

你好,我是彤哥。面试

上一节,咱们一块儿学习了关于哈希的一切,特别是哈希表的进化过程,相信经过上一节的学习,你必定能够从头至尾完整地给面试官讲讲哈希表是如何发展到现在这一步的。算法

可是,难道HashMap的终极形态只能经过“数组+链表+红黑树”的形式实现吗?有没有可替代方案?为何Java没有使用你说的这种替代方案呢?数组

本节,咱们就来学习另一种数据结构——跳表,关于跳表的内容,我将分红两节完成,第一节介绍跳表的演进过程,第二节代码实现跳表,并改写HashMap。数据结构

好了,让咱们先进入跳表第一小节的学习。架构

有序数组

你们都知道数组是能够支持随机访问的,也就是经过下标能够快速地定位到元素,时间复杂度是O(1)。ide

那么,这个随机访问的特性除了根据下标查找元素,还具备哪些用处呢?源码分析

试想,若是一个数组是有序的,我要查找某个指定的元素,如何才能作到最快速地查找出来呢?学习

谁说有序链表不能进行二分查找,只是须要进化而已?!

简单地方法,从头开始遍历整个数组,遇到了要查找的元素就返回,好比,查找8这个元素,要走6次才能查找到,要查找10这个元素更夸张,须要8次。优化

因此,这种方式的查找元素的时间复杂度为O(n)。

1

快速地方法,由于数组自己是有序的,因此,咱们可使用二分查找,先从中间开始查找,若是指定元素比中间的元素小,再在左半边查找,若是指定元素比中间元素大,则在右半边查找,依次进行,直到找到指定元素。好比,查找8这个元素,先定位到中间(7/2=3)的位置,下一次查找让左指针加1,把4号位置做为左指针,中间的位置变为(4+(7-4)/2=5)的位置,查找到8这个元素,一共只须要2次。

使用二分查找,效率提高了不止一星半点,即便最坏的状况也只须要log(n)的时间复杂度。

2

有序链表

上面咱们介绍了有序数组的快速查找,下面咱们再来看看有序链表的状况。

3

上面是一个有序链表,此时,我要查找8这个元素,只能从链表头开始查找,直到遇到8为止,时间复杂为O(n),彷佛没有什么更好地办法了。

4

让咱们考虑有序数组和有序链表的不一样之处,有序数组之因此可以实现能够直接定位到中间元素,得意于其能够经过索引(下标)快速访问的特性,那么,咱们给有序链表加上索引是否是就能够实现相似的功能了呢?

答案是确定的,这种具备索引的有序链表就是跳表,下面有请跳表登场。

跳表

第一个问题:怎么给有序链表加索引呢?

这里,须要增长一个“层”的概念,假设原始链表的层级为0,那么,在其中选择一些元素向上延伸,造成第1层索引,一样地,在第1层索引的基础上,再选择一些元素向上延伸,造成第2层索引,直到你以为索引的层数差很少了为止,没错,跳表就是这么随意,你满意就好^^

假设,针对上面的有序链表,我加了这么一些索引:

5

第二个问题:从哪开始访问这个跳表呢?6?3?1?9?

好像都不行,因此,还要增长一个特殊的节点——头节点,放在0号元素的前面,好比,上面的跳表增长头节点以后的样子以下:

6

此时,只要从h2这个节点开始,就能很快速地查找到跳表中的任意一个元素。

好比,要查找8这个元素,h2先向右看一下,咦,是6,比8小,跳到6这个位置,再向右看一下,啊,是9了,比8大了,因此,不能跳过去,向下跳一步,跳到第1层6的位置,向右看一下,又是9,不能跳过去,再向下跳一步,到第0层的6,既然,到第0层,那只能按照链表依次日后遍历了,直到遇到8为止,整个过程以下:

7

能够看到,整个过程就是跳呀跳呀跳,因此得名——跳表。

这里的元素个数比较少,可能还看不出太大的优点,试想,若是元素很是多,每两个元素向上造成一个索引,每两个索引再向上造成一个索引,最后,就相似于一颗平衡二叉树了:

8

能够看到,每次查找能够减小一半的搜索范围,因此,跳表的查询时间复杂度为O(log n)。

可是,实际状况是不可能使用这种彻底平衡的跳表的,由于,若是要保持平衡的特性,在插入元素或删除元素的时候势必须要作再平衡的操做,这样就大大地下降了效率,因此,通常地,咱们使用随机来决定一个元素或者索引要不要产生索引。

第三个问题:索引什么时候产生呢?

最好的时机莫过于插入元素的时候,由于在插入元素以后的下一步就要立马使用索引了,为何这样说呢?由于不论是插入、删除仍是查询,其实,都要先走查询找到那个元素才能进行下一步操做。说白了,就是无论什么操做,都要查询,是查询就要走索引,要走索引就要先建索引,要建索引那就在插入元素的时候。

OK,下面我将使用一步一图的方式,带你领略跳表建立的完整过程:

  1. 初始状态,只有一个头节点h0(不,还有一个彤哥读源码的水印,调皮^^)。

    9

  2. 插入一个元素4,放在h0后面,并随机决定要不要向上造成索引,结果是不造成索引。

    10

  3. 插入一个元素3,从h0开始查找,h0的下一个元素是4,比3大,因此,3放在h0和4之间,而后询问要不要造成索引,随机决定说要造成索引,此时,3向上造成索引,同时,h0也要向上造成索引h1,结果以下:

    11

  4. 插入一个元素9,从h1开始查找,依次通过h1->3->3->4,都没有找到位置,最后插入到4后面,并询问要不要造成索引,随机决定说我要造成索引,并且我要造成2层索引(最多比当前层数多1),而后就变成了这个样子:

    12

  5. 接着,插入了元素1和7,它们都无惊无喜,没有造成索引:

    13

  6. 插入元素6,根据索引,查找路线为,h2->h1->3->3->4,咦,发现4下一个是7了,因此,6放在4和7之间,而后,决定要不要造成索引,随机决定说我要造成索引,并且我也要造成2层索引,这时候就很麻烦了,在造成6这个元素索引的时候,须要修改3->9这条线,还要修改h2->9这条线,生成的结果以下:

    14

  7. 后面,插入了元素8和10,都是无惊无险,没有产生任何索引,因此,最后的结果以下:

    15

能够看到,跳表是一个很是随意的数据结构,即便按照一样的顺序从新插入一遍元素,生成的跳表也可能彻底不同,任性,因此,我很喜欢跳表这种数据结构。

第四个问题:上面描述了插入元素的过程,删除过程是怎么样的呢?

删除过程,首先也要查找到元素,可是,有一点点小区别,很是小的区别,很难描述,好比,要删除6这个元素,我能不能从h2->6->6->6这个路径过来呢?

不能,由于从这条路径过来,删除第1层的索引6后,没法修复3->9这条线,因此,删除元素的时候只能走h2->h1->3->3->4->6这条路径,且把途中每一层最后通过的索引记住,才能在删除了6这个元素以后正确地修复各层的索引。

删除6以后的样子以下:

16

咦,讲到这里,我不经想起了Java跳表ConcurrentSkipListMap中的一个小优化项,在ConcurrentSkipListMap中,不论是查找、插入,仍是删除,都是走的跟删除相同的查找路径,其实,能够简单地优化一下,插入和查找的时候彻底能够走另外一条路径。

17

有兴趣的同窗能够扒一下个人源码分析:死磕 java集合之ConcurrentSkipListMap源码分析

好了,关于跳表的理论知识咱们就讲解到这里。

后记

本节,咱们经过一步一图的方式完整清晰地展现了跳表查找、插入、删除元素的全过程,你有没有Get到呢?能吊打面试官了么?

然而,不少同窗可能会说“Talk is cheap, Show me the code”,OK,下一节,我就将用代码的方式给你展示跳表实现的细节,并使用跳表改写HashMap,Next Part 见。

关注公主号“彤哥读源码”,解锁更多源码、基础、架构知识。