二分问题模板
二分查找是不是玄学编程
寻找一个数字
最简单的场景,寻找一个数字返回索引
int binarySearch(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 注意
while(left <= right) {//思考1:为什么是小于等于
int mid = left + (right - left) / 2;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1; // 思考2:为什么是mid+1
else if (nums[mid] > target)
right = mid - 1; // 思考3:为什么是mid-1
}
return -1;
}
思考一:为什么while中是小于等于
while中的区间表示的是查找区间,并且right的初始值为nums.length-1。当条件为left<=right时,显而易见退出的区间为[right+1,right],即查找区间为空的时候,退出查找。如果条件改为left<right这种错误形式,那么就会存在这样一种情况,当left=right=nums.length-1,查找就会退出,而恰好我们寻找的值就处于数组最后一位,因此程序因为查找不到target而返回-1,程序出错。
思考二和思考三:为什么有的程序中更新边界值为mid,有的范围进行缩小呢?
首先需要明确一个概念,在搜索过程中,我们的搜索区间是[left,right]一个闭区间,如果发现当前mid不符合我们的查找条件,那么我们下一步一定实在[mid+1,right]或者[left,mid-1]这个区间中搜索,因此每次查找的值不符合条件需要将边界条件进行缩小。
寻找左边界的二分查找(思考一)
int left_bound(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0;
int right = nums.length; // 思考二
while (left < right) { // 思考三
int mid = (left + right) / 2;
if (nums[mid] == target) {
right = mid;//思考六
} else if (nums[mid] < target) {
left = mid + 1;//思考五
} else if (nums[mid] > target) {
right = mid; // 思考四
}
}
return left;
}
思考一:什么是左边界的二分查找
左边界可以理解为包括左边界在内的右侧值都满足查找条件,例如有一个数组[-1,2,3,4,5,6],输入target的值为1,那么程序最终返回的结果就是1,表示的是从index=1开始,右侧的值都满足大于等于1这个条件。
思考二和思考三:为什么右边界的值是nums.length()而不是nums.length()-1以及为什么while中是<而不是<=
这两个思考配套来想,因为我们的问题是寻找左边界,搜索区间可以定义为[left,right),即目标值出现的位置永远在一个左闭右开区间之中,这就是为什么while中的条件需要是left<right,而不是left<=right。我们的右边界需要初始化需要定义为nums.length,否则就会丢失数组中最后一个index=nums.length()-1的搜索。
思考四和思考五:为什么这里的边界值right=mid,left=mid+1
结合搜索区间来想,每次搜索搜索区间都会被划分为[left,mid),[mid,right),二者都是左闭右开,代码中这样设置边界值,可以做到不重不漏的搜索。
思考六:为什么这个算法可以寻找左闭右开的区间呢
这是因为当nums[mid]==target的时候,没有立即返回而是缩小了搜索区间的上界,即rigth=mid,这样的话,下次的搜索就会在[left,right)中进行。
当然,while的条件也可以设置为<=,聪明的读者可以自行补充
寻找右边界的二分查找(思考一)
int right_bound(int[] nums, int target) {
if (nums.length == 0) return -1;
int left = 0, right = nums.length;
while (left < right) {
int mid = (left + right) / 2;
if (nums[mid] == target) {
left = mid + 1; // 思考二
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid;
}
}
return left - 1; // 思考三
}
该算法的搜索区间仍然采用左闭右开区间,算法的实现与寻找左边界雷同。
思考一:什么是右区间的二分查找
可以参考左区间的二分查找,右区间的二分查找就是当一个序列左边的值都满足条件的时候,我们需要找到满足条件最右边的那个值。
思考二:算法是怎么实现可以找到右边界的?
当nums[mid]==target的时候,我们需要缩小搜索区间,因此将left=mid+1,一直缩小满足条件的区间。可以设想一下,如果left=mid,这样的话,区间最终会被缩小到一个值mid,从而陷入死循环,即每次都会执行left=mid无法结束循环。
思考三:为什么最后的返回值为left-1,而不是left
正如思考二所言,满足条件的时候,left=mid+1,所以最后当left=right退出的循环的时候,left指向的位置是满足条件的后一位,所以需要返回left-1的操作。
总结:二分问题三步走
- 判断问题区间为寻找某一个值,还是左边界,亦或是右边界。
- 根据问题区间,思考每次搜索将大区间,划分为哪两个小区间,从而设定每个条件判断中的边界设定。
- 根据while跳出时的边界关系,判断返回值。
小试牛刀:
题目链接:https://leetcode-cn.com/problems/magnetic-force-between-two-balls/
题目描述:Rick 有 n 个空的篮子,第 i 个篮子的位置在 position[i] ,Morty 想把 m 个球放到这些篮子里,使得任意两球间最小磁力最大。已知两个球如果分别位于 x 和 y ,那么它们之间的磁力为 |x - y| 。给你一个整数数组 position 和一个整数 m ,请你返回最大化的最小磁力。
解法分析:本题最终结果是最近的两个球的距离,换言之,求怎样摆放能使最近的两个球距离最大。可以使用二分答案的思想,即设置答案区间为1到position[position.size()-1],二分答案区间,显而易见,答案是一个右边界,即较小的答案满足情况下,我们需要找到一个最大值最为最终答案。查找到一个距离,使用贪心的思想,遍历position排序后数组当两个桶的距离大于等于目标距离即放球,以此来check搜索到的的答案是否满足题意。
AC代码:
class Solution {
public:
bool check(vector<int>& position, int dis, int m){
int cnt = 1;
int p = 0;
for(int i=1;i<position.size();i++){
if(position[i]-position[p]>=dis){
p = i;
cnt ++;
}
if(cnt>=m){
return true;
}
}
return false;
}
int maxDistance(vector<int>& position, int m) {
sort(position.begin(),position.end());
int left = 1;
int right = position[position.size()-1];
int mid = 0;
while (left < right) {
mid = left + ((right - left) >> 1);
bool res = check(position,mid,m);
if(res==true) //如果当前的mid满足条件,说明[left,mid]都满足,缩小区间为[mid+1,right)
left = mid + 1;
else
right = mid;//如果当前的mid不满足,说明[mid,right)都不满足,缩小区间为[left,mid)
}
return left-1;
}
};