一般的书在讲解二分法的时候都是以在有序数组中查找目标值为例子来讲解。但是实际上二分法的应用可以更广泛。
给定任意一个数组,只要该数组能够根据某个判定条件将区间分成两段,一段满足该判定条件,另一段不满足该判定条件,那么我们就可以用二分法查找到两段的临界点。
下面以用二分法开方为例。
Implement int sqrt(int x).
Compute and return the square root of x, where x is guaranteed to be a non-negative integer.
Since the return type is an integer, the decimal digits are truncated and only the integer part of the result is returned.
Example 1:
Input: 4
Output: 2
Example 2:
Input: 8
Output: 2
Explanation: The square root of 8 is 2.82842..., and since
the decimal part is truncated, 2 is returned.
对于这个问题,直观的思路就是通过二分法试错,找到解。
我们可以将判定条件设为数组元素t的平方是否小于等于x。根据这个条件,我们可以将数组分成两段,左侧的一段满足该条件,右侧的一端不满足该条件。并且不论x是否为平方数,我们想要的解都在左侧那段的右端点上。
算法Java代码如下:
public int mySqrt(int x) {
int l = 0, r = x;
while(l
代码中有几点需要解释。
第一点:程序跳出while循环的时候一定是l==r。
考虑这样几种情况。
第一种情况:l刚刚好等于r-1,此时mid值为r,将会导致更新r,其结果导致l==r,然后退出循环。
第二种情况:r-l==2,此时mid一定是在l与r之间,如果执行第6行,则导致l==r-1,变成了第一种情况,如果执行第8行,则导致l==r,直接退出循环。
第三种情况:r-l>2,此时mid在l和r之间,l或r经过更新之后要么还是第三种情况,要么回到前两种情况。所以最终退出循环的时候一定是l==r。
第二点:为什么l(或r)就是最终的结果?
从第一点中的前两种情况可以看到,当l==r时l就是左侧区间的右端点,即是目标解。
第三点:mid为什么是向上取整?
如果mid向下取整 ,在l==r-1时会导致mid值为l,从而导致执行第6行,从而导致l的值没有更新,产生死循环。
对于上述求开方的问题,我们还可以根据数组元素的平方是否大于x将区间[0,x]划分成两段,左边的一段[0,⌊sqrt(x)⌋]不满足该条件,右边的一段[⌊sqrt(x)⌋+1,x]满足该条件;并且我们要找的目标值为左边这一段的右端点。这种思路的代码和上述代码完全一样。
注意:不能将判定条件设为数组元素的平方是否大于等于x。如果这样设定,那么当x为平方数时,目标结果在右侧区间的左端点上;而当x不是平方数时,目标解在左侧区间的右端点上。这样的话就导致代码需要特殊判断,不如前面提到的方法简洁。
再看一道二分法应用题。
Given a sorted array and a target value, return the index if the target is found. If not, return the index where it would be if it were inserted in order.
You may assume no duplicates in the array.
Example 1:
Input: [1,3,5,6], 5
Output: 2
Example 2:
Input: [1,3,5,6], 2
Output: 1
Example 3:
Input: [1,3,5,6], 7
Output: 4
Example 4:
Input: [1,3,5,6], 0
Output: 0
我们将判定条件设为数组元素是否大于等于target,则可以将数据分成两部分,左侧部分的数组元素均小于target,右侧部分的数组元素均大于等于target。如果数组中存在target,那么一定是右侧部分的左端点,要求的索引就是该点的索引;如果数组中不存在target,则右侧部分的左端点一定是大于target的最小值,target的插入位置就是该点的位置,所以要求的索引就是该点的索引。综合起来,最终的解就是右侧部分左端点的索引。
算法Java代码如下:
public int searchInsert(int[] nums, int target) {
// 特殊情况判断
if(nums.length == 0 || target>nums[nums.length-1]) return nums.length;
int l = 0, r = nums.length - 1;
while(l < r) {
int mid = (l+r) / 2; // 向下取整
if(nums[mid] >= target){
r = mid;
}else{
l = mid + 1;
}
}
return r;
}
注意:这道题在求mid时是向下取整,因为如果向上取整的话,在l=r-1时,mid的值则为r,并执行第8行代码,导致r实质上并未更新,从而导致死循环。
通过这两道题,我们可以看到二分法不仅仅可以用于有序数组的查找,只要数组可以通过某个条件进行二分时都可以考虑使用二分法。首先想一个能够将数组划分成两部分的判定条件,并且该条件要能保证目标解在其中一部分的端点上。然后就可以将判定条件套进上述代码中,并注意mid的取法,保证不会有死循环就行了。
小技巧:代码中的除2操作可以用右移1位来实现,效率更高:)
欢迎关注博主个人微信公众号~~~