问题定义:如何不死记二分的写法
以下的讨论都是基于问题的解是存在的前提下。
场景一:在一个有序的数组中寻找某个下标使得其对应的值等于某个指定的target
对于二分查找最常见的应用场景就是在一个有序的数组中寻找某个下标使得其对应的值等于某个指定的target。如下的代码是最简单的,也是最容易记忆的一种写法。
public static int binarySearch(int[] arr, int target){
int l = 0;
int r = arr.length-1;
while(l<=r){
int mid = l + (r-l)/2;//防止溢出
if(arr[mid]==target) return mid;
else if(arr[mid]<target) l = mid + 1;
else r = mid - 1;
}
return l;
}
上面的这种二分场景,多用于数组中不含有重复元素(或者含有重复元素,当解有多个的时候,随机返回其中的一个下标)。由于不含有重复元素,因此在二分查找的过程中,对于解出现在数组的中间(非数组的边界)时,只有mid下标才是可能的解,因此在每次二分更新mid的时候,mid总要加1或者是减去1。考虑到边界情况,如果问题的解出现在数组的最左边和或者最右边的情况,因此需要考虑l和r为问题的可能解,因此终止条件是l<=r,而不是l<r。
场景二:在一个有序的数组中,左边第一个大于等于目标值target的下标,即左边界问题,也是最大的最小问题
。
首先在写代码之前,我们要明白:
m
i
d
=
l
+
(
r
−
l
)
/
2
mid = l + (r-l)/2
mid=l+(r−l)/2它是向下取整的,即l和r挨边时,mid是往左偏的。考虑一种场景,当l和r挨边的时候,此时的mid总是取到左边l,而取不到右边r。
先看下代码再进行代码分析。
public static int binarySearchUpper(int[] arr, int target){
int l = 0;
int r = arr.length-1;
while(l<r){
int mid = l + (r-l)/2;
if(arr[mid]<target) l = mid + 1;
else r = mid;
}
return l;
}
问题分析:
- 为什么等于mid的时候不直接返回?
因为该种场景下,可能存在多个数组值等于target,但是,我们是要求出最左边的那个下标,因此,当arr[mid]==target的时候,不是直接返回,而是接着往左寻找(丢弃右半部分),更新r。当arr[mid]==target, 此时的mid有可能就是最左边应该返回的答案,因此为了防止继续往左查找的时候不漏掉这个解,故将r更新为r = mid, 而不是mid-1。 - 为什么r 更新为 mid而不是更新为mid-1?
分析如上 - 为什么l更新为mid + 1?
由于是要寻找左边第一个大于等于指定值target所对应的下标,那么当arr[mid]<target时说明mid的左边包括mid所对应的位置是肯定不满足条件的,因此在舍弃左半部分之后,进行更新l的时候,l = mid + 1。 - 为什么循环的终止条件是l < r,而不是l<=r?
反证,如果循环结束条件是l<=r,如前面所述, m i d = l + ( r − l ) / 2 mid = l + (r-l)/2 mid=l+(r−l)/2它是向下取整的,因此mid总是往左偏,当l和r相等的时候,此时mid总是等于l, 而当arr[mid] == target的时候,我们由于没有直接返回,而是令r = mid,也就是r == l.故造成死循环。
场景三: 在一个有序的数组中,右边第一个小于等于目标值target的下标,即右边界问题,也是最小的最大问题
。
public static int binarySearchLower(int[] arr, int target){
int l = 0;
int r = arr.length-1;
while(l<r){
int mid = l + (r-l+1)/2;
if(arr[mid]>target) r = mid-1;
else l = mid;
}
return l;
}
可以参考场景二进行做相同的三个问题思考,这里只有一点需要简单提一下的是为什么 m i d = l + ( r − l + 1 ) / 2 mid = l + (r-l+1)/2 mid=l+(r−l+1)/2而不是 m i d = l + ( r − l ) / 2 mid = l + (r-l)/2 mid=l+(r−l)/2,此处多加一个1,是为了让当l和r挨边的时候,往右边偏,因为该问题是求右边界的问题,考虑数组[2, 2], target==2,此时,由于要求右边界,因此,mid应该向上取整,故二分的时候分子多加了一个1。
总结: 对于二分问题的代码,只需要记住如下的关键点。
- 判断问题是求左边界还是求右边界,从而确定mid往哪偏。
- 依据问题是求左边界还是右边界,从而确定在二分淘汰的时候是更新谁,保留谁为mid。
- 其实很多问题都可以转化为最优解问题,比如最大中的最小,最小中的最大,都可以转化为场景一和场景二,只是将if条件单独封装成一个函数,将问题的判断单独考虑,其他代码不变。相应的leetcode问题有:
分割数组的最大值
在D天内运送包裹的能力
小张刷题计划
相信通过上分析,我们能更好更准确地写出二分代码。