二分查找
¶二分查找算法概述
二分查找很简单吗?或许二分查找的思想是直观的,但是细节才是程序的核心。看看 Knuth 大佬(发明 KMP 算法的那位)怎么说的:
Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky…
¶二分代码的框架
1 | int binarySearch(int[] nums,int target){ |
这里虽然可以用else来简化代码,但是为了更好的理解二分查找的逻辑,这里还是用了if-else的写法。这样可以保留更多的二分比较的细节,作为初学者来说更易理解。
其中**…**标记的部分,就是可能出现细节问题的地方,当你见到一个二分查找的代码时,首先注意这几个地方。后文用实例分析这些地方能有什么样的变化。
另外声明一下,计算 mid 时需要技巧防止溢出,建议写成: mid = left + (right - left) / 2
,可以暂时先不管这个问题,后文会详细解释。
¶二分查找一个数
二分查找的最基础应用,即给定一个有序数组,查找某个数是否存在,如果存在则返回其索引,否则返回-1。
1 | int binarySearch(int nums[],int target){ |
¶Q1:为什么 while 循环的条件中是 <=,而不是 < ?
因为初始化 right 的赋值是数组是最后一个元素的索引,这里使用的数组下标是从1开始的。
如果使用零下标数组,那应该这样写:
1 | int left = 0; |
使用<=
相当于二分查找的区间是左闭右闭的区间,[left,right]
,而使用<
则相当于左闭右开的区间[left,right)
在以0下标开始的数组来说索引大小为n是越界的。
下面没有特殊说明我们都按照左闭右闭区间来说明。
这个算法中使用的是 [left, right] 两端都闭的区间。这个区间就是每次进行搜索的区间,我们不妨称为「搜索区间」(search space)。
什么时候应该停止搜索呢?当然,找到了目标值的时候可以终止:
1 | if(nums[mid] == target) |
但如果没找到,就需要 while 循环终止,然后返回 -1。那 while 循环什么时候应该终止?搜索区间为空的时候应该终止,意味着你没得找了,就等于没找到嘛。
while(left <= right)的终止条件是 left == right + 1,写成区间的形式就是 [right + 1, right],或者带个具体的数字进去 [3, 2],可见这时候搜索区间为空,因为没有数字既大于等于 3 又小于等于 2 。所以这时候 while 循环终止是正确的,直接返回 -1 即可。
while(left < right)的终止条件是 left == right,写成区间的形式就是 [right, right],或者带个具体的数字进去 [2, 2],这时候搜索区间非空,还有一个数 2,但此时 while 循环终止了。也就是说这区间 [2, 2] 被漏掉了,索引 2 没有被搜索,如果这时候直接返回 -1 就可能出现错误。
当然,如果你非要用 while(left < right) 也可以,我们已经知道了出错的原因,就打个补丁好了:
1 | //... |
但是,这种方式不如 <= 来的直观
¶Q2:为什么 left = mid + 1,right = mid - 1?
我看有的代码是 right = mid 或者 left = mid,没有这些加加减减,到底怎么回事,怎么判断?
答:这也是二分查找的一个难点,不过只要你能理解前面的内容,就能够很容易判断。
刚才明确了「搜索区间」这个概念,而且本算法的搜索区间是两端都闭的,即 [left, right]。那么当我们发现索引 mid 不是要找的 target 时,如何确定下一步的搜索区间呢?
当然是去搜索 [left, mid - 1] 或者 [mid + 1, right] 对不对?因为 mid 已经搜索过,应该从搜索区间中去除。
那为什么有些是right = mid 或者 left = mid呢?很明显哪些代码用的是左闭右开的搜索区间,而我们这里用的是左闭右闭的搜索区间