对于长度太小的有序数组,二分查找并不比线性查找好多少。但我们来看看更大的数组。
对于拥有100个值的数组来说,两种查找需要的最多步数如下所示。
❏ 线性查找:100步
❏ 二分查找:7步
用线性查找的话,如果要找的值在最后一个格子,或者比最后一格的值还大,那么就得查遍每个格子。有100个格子,就是100步。
二分查找则会在每次猜测后排除掉一半的元素。100个格子,在第一次猜测后,便排除了50个。
再换个角度来看,你就会发现一个规律。
长度为3的有序数组,二分查找所需的最多步数是2。
若长度翻倍,变成7(以奇数为例会方便选择正中间的格子,于是我们把长度翻倍后又增加了一个数),则最多步数会是3。
若再翻倍(并加1),变成15个元素,那么最多步数会是4。
规律就是,每次有序数组长度乘以2,二分查找所需的最多步数只会加1。
相反,在3个元素的数组上线性查找,最多要3步,7个元素就最多要7步,100个元素就最多要100步,即元素有多少,最多步数就是多少。数组长度翻倍,线性查找的最多步数就会翻倍,而二分查找则只是增加1步。
这种规律可以用下图来展示。
如果数组变得更大,比如说10000个元素,那么线性查找最多会有10000步,而二分查找最多只有14步。再增大到1000000个元素,则线性查找最多有1000000步,二分查找最多只有20步。
有序数组并不是所有操作都比常规数组要快。如你所见,它的插入就相对要慢。衡量起来,虽然插入是慢了一些,但查找却快了许多。你得根据应用场景来判断哪种更合适。
二分法的大O记法
它不能写成 O(1),因为二分查找的步数会随着数据量的增长而增长。它也不能写成 O(N),因为步数比元素数量要少得多,正如之前我们看到的,包含100个元素的数组只要7步就能找完。
看来,二分查找的时间复杂度介于O(1)和O(N)之间。
好了,二分查找的大O记法是:
O(logN)
可将其读作“O log N”。归于此类的算法,它们的时间复杂度都叫作对数时间。
简单来说,O(log N)意味着该算法当数据量翻倍时,步数加1。这确实符合之前章节我们所介绍的二分查找。
目前为止所接触的三种时间复杂度,按照效率由高到低来排序的话,会是这样:
O(1)
O(logN)
O(N)
下图为它们三者的对比。注意O(log N)曲线的微弯,使其效率略差于O(1),却远胜于O(N)。
解释O(log N)
在大O记法中,当我们说O(log N)时,其实指的是O(log2N),不过为了方便就省略了2而已。你应该还记得O(N)代表算法处理N个元素需要N步。如果元素有8个,那么这种算法就需要8步。
O(logN)则代表算法处理N个元素需要log2N步。如果有8个元素,那么这种算法需要3步,因为log28=3。从另一个角度来看,如果要把8个元素不断地分成两半,那么得拆分3次才能拆到只剩1个元素。
这正是二分查找所干的事情。它就是不断地将数组拆成两半,直至范围缩小到只剩你要找的那个元素。
简单来说,O(logN)算法的步数等于二分数据直至元素剩余1个的次数。
下表是O(N)和O(log N)的效率对比。
每次数据量翻倍时,O(N)算法的步数也跟着翻倍,O(log N)算法却只需加1。
学会大O记法,我们在比较算法时就有了一致的参考系。有了它,我们就可以在现实场景中测量各种数据结构和算法,写出更快的代码,更轻松地应对高负荷的环境。