第一次写文章,如果有说的不对的地方欢迎大佬们指正
基础的二分法
前提:有序数组
形式:二分法一般有两种写法,分别对应不同的区间定义。
区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。
一种是左闭右闭,即[left, right],另一种是左闭右开,即[left, right)
第一种写法:[left, right],即target是在这个闭区间内。
首先,while的循环条件必须是left <= right,因为只有left = right的时候,区间的右端点(i.e. right)才能被考虑到。因为在left < right的时候,(left + right) >> 1 < right,即mid < right,所以这时永远不可能考虑到right这个右端点。只有当left = right的时候, mid = right,我们才考虑到了右端点,所以这个等于的情况是有意义的,必须取等号。
其次,循环内判断的时候if(nums[mid] > target),也就是说明target值在[left, mid)区间内,由于我们考虑的是[left, right]这个闭区间,所以此时right应该置为mid - 1。
这种写法最后终止时right = left - 1;
第二种写法:[left, right)
首先,while循环条件是left < right,因为left = right的情况对我们没有意义,我们不考虑right这个右端点。
其次,right一开始必须初始化为数组长度n而不是n - 1。
最后,if(nums[mid] > target),right应该更新为 mid,因为寻找区间是左闭右开区间,而nums[mid]不等于target,所以right更新为mid。
这种写法最后终止时left = right。
二分法的变式
要对二分法进行变式,关键是注意循环时不同if情况下指针的移动和循环最终终止时left和right的关系。
以leetcode题目34. 在排序数组中查找元素的第一个和最后一个位置为例:
这个题目中target有重复的值,所以主要修改两个基础写法最终定位到的target的位置。
这里参考了leetcode用户jys_std的评论代码。我们可以将查找第一个位置理解为查找大于等于target的第一个位置,而查找最后一个位置就是查找大于等于target + 1的第一个位置,这样就实现了统一。
我们知道基础的二分法是查找一个单独元素的位置,那么如何修改才能让它实现查找大于等于给定元素的第一个位置呢?
这里以第二种写法为例(左闭右开)。
这是基础二分法的样例java代码:
public int search(int[] nums, int target) {
int left = 0, right = nums.length;
while(left < right){
int mid = (left + right) >> 1;
if(nums[mid] < target) left = mid + 1;
if(nums[mid] > target) right = mid;
if(nums[mid] == target) return mid;
}
return -1;
}
再给出此题的变式代码:
//找>=target的第一个
public int search(int[] nums,int target){
int l = 0, r = nums.length;
while(l < r){
int mid = (r + l) >> 1;
if(nums[mid] >= target)
r = mid;
else
l = mid + 1;
}
return l;
}
上面提到了二分法的变式关键是注意循环时不同if情况下指针的移动和循环最终终止时left和right的关系
首先,在nums[mid] = target的时候,我们不能return,因为会有多个target,那么此时我们要找的是最左边的target,而我们不知道这个是不是最左边,但是可以确认的是右区间(mid, right]不用考虑了,而只要考虑[left, mid)这个区间。所以此时我们将right置为mid.
然后考虑nums[mid] > target的情况,此时和第一种情况一样,也是把right置为mid。
接着我们看nums[mid] < target的情况,此时target肯定在(mid, right)中,所以我们将left置为mid + 1;
最后我们关注循环终止时的情况,也就是left = right。此时指针会停在最左边的target的位置,因为基于上面的条件判断,nums[mid]等于target的时候,我们是将right左移,考虑左区间。最后因为left = right,我们返回任意一个就行。
当然此题还要考虑target值超出nums数组界限的情况:
public int[] searchRange(int[] nums, int target) {
int l = search(nums,target);
int r = search(nums,target + 1);
if(l == nums.length || nums[l] != target)
return new int[]{-1, -1};
return new int[]{l, r - 1};
}
l == nums.length对应target大于nums最大值的情况,nums[l] != target对应target小雨nums最小值的情况。
如果想练练手还可以去做做69. x 的平方根 。
参考: