跳到主要内容

3.1 算法解释

二分查找也常被称为二分法或者折半查找 (binary search, bisect),每次查找时通过将待查找的单调区间分成两部分并只取一部分继续查找,将查找的复杂度大大减少。对于一个长度为 O(n)O(n) 的数组,二分查找的时间复杂度为 O(logn)O(\log n)

举例来说,给定一个排好序的数组 {3,4,5,6,7}\{3,4,5,6,7\},我们希望查找 4 在不在这个数组内。第一次折半时考虑中位数 5,因为 5 大于 4, 所以如果 4 存在于这个数组,那么其必定存在于 5 左边这一半。于是我们的查找区间变成了 {3,4,5}\{3,4,5\}。(注意,根据具体情况和您的刷题习惯,这里的 5 可以保留也可以不保留,并不影响时间复杂度的级别。)第二次折半时考虑新的中位数 4,正好是我们需要查找的数字。于是我们发现,对于一个长度为 5 的数组,我们只进行了 2 次查找。如果是遍历数组,最坏的情况则需要查找 5 次。

我们也可以用更加数学的方式定义二分查找。给定一个在 [a,b][a, b] 区间内的单调函数 f(t)f(t),若 f(a)f(a)f(b)f(b) 正负性相反,那么必定存在一个解 cc,使得 f(c)=0f(c) = 0。在上个例子中,f(t)f(t) 是离散函数 f(t)=t+2f(t) = t + 2,查找 4 是否存在等价于求 f(t)4=0f(t) - 4 = 0 是否有离散解。因为 f(1)4=34=1<0f(1) - 4 = 3 - 4 = -1 < 0f(5)4=74=3>0f(5) - 4 = 7 - 4 = 3 > 0,且函数在区间内单调递增,因此我们可以利用二分查找求解。如果最后二分到了不能再分的情况,如只剩一个数字,且剩余区间里不存在满足条件的解,则说明不存在离散解,即 4 不在这个数组内。

具体到代码上,二分查找时区间的左右端取开区间还是闭区间在绝大多数时候都可以,因此有些初学者会容易搞不清楚如何定义区间开闭性。这里笔者提供两个小诀窍,第一是尝试熟练使用一种写法,比如左闭右开(满足 C++、Python 等语言的习惯)或左闭右闭(便于处理边界条件),尽量只保持这一种写法;第二是在刷题时思考如果最后区间只剩下一个数或者两个数,自己的写法是否会陷入死循环,如果某种写法无法跳出死循环,则考虑尝试另一种写法。

二分查找也可以看作双指针的一种特殊情况,但我们一般会将二者区分。双指针类型的题,指针通常是一步一步移动的,而在二分查找里,指针通常每次移动半个区间长度。