二分查找模板
首先先上模板,y总(B站大雪菜)总结出来了两套二分的代码,基本能解决九成左右的二分问题.二分在特别的边界问题上可以选用此模板作为参考。当然在一些简单的边界问题上,可以自己思考边界来自己写代码。
版本一
当我们将区间
[l, r]
划分成[l, mid]
和[mid + 1, r]
时,其更新操作是r = mid
或者l = mid + 1
;计算mid
时不需要加1
。
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1; //除2 操作
if (check(mid)) r = mid; //check函数即为边界的选择
else l = mid + 1;
}
return l; //l r都可以,跳出while r = l
}
版本二
当我们将区间
[l, r]
划分成[l, mid - 1]
和[mid, r]
时,其更新操作是r = mid - 1
或者l = mid
;此时为了防止死循环,计算mid
时需要加1
。
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1; //注意+1操作
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
代码解释(来自B站大雪菜老师讲解,加些自己的理解)
如图我们把一个闭区间
[l, r]
,分为左右两端(我们要明白,对于二分算法来说,数据在某种意义上是有序的
,或者是直接有序,例如非递减
。或者是以其他的形式有序,例如旋转有序
。)再者,因为二分是抛弃一段,从另一段中继续寻找答案,所以我们就可以把整个过程一直当做是两段中选一段。其实分段的这个过程就是check()
函数的确定。① ① ①对于
模板一
,我们就好比在查找图中绿色段中的左端点。如果我们的mid
在红色段,肯定是不符合查找需求的,让l = mid + 1
。如果mid
是在绿色段,即符合我们的查找需求,但不是结束点,让r = mid
。
② ② ②对于模板二
,我们就好比在查找图中红色段中的右端点。如果我们的mid
在绿色段,肯定是不符合查找需求的,让r = mid - 1
。如果mid
是在红色段,即符合我们的查找需求,但不是结束点,让l = mid
。此时我们需要注意一点。即陷入死循环,举例:我们取下标l = r - 1
,如果我们在计算时选择下取整(直接除2),m = (l + r) / 2 = (2 * l + 1) / 2 = l
,我们再次更新l = mid
,就陷入了死循环。
例题
class Solution {
public:
int mySqrt(int x) {
int l = 0, r = x;
while(l < r)
{
int mid = (long long)l + r + 1 >> 1; //check函数 mid * mid <= x
if(mid <= x / mid) l = mid;
else r = mid - 1;
}
return l ;
}
};
此时有读者按照上面的讲解,就在想,
check
函数能不能使用mid * mid >= x
。当然这种想法是没问题的。现学现用,但是我们来看这个题目的样例二,明显的给我们表明了,我们对于不能整数开平方的数来说,需要舍去小数,即下取整。当我们使用 mid * mid >= x 当x
是整数的开平方数的话,自然是没有问题的,但当x
不能被整开平方,我们的这个check
最后取得结果就是上取整
,不符合题意。
所以当我们在选择check
条件的时候也不是随便两个模板换着用,一切以符合题意为主。
这个题目就是简单的寻找有序数组中第一个大于等于
target
的位置,c++STL
中有lower_bound()
return lower_bound(nums.begin(),nums.end(),target) - nums.begin();
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
if(nums.empty() || nums.back() < target) return nums.size();
int l = 0, r = nums.size() - 1;
while(l < r)
{
int mid = (l + r) / 2;
if(nums[mid] < target) l = mid + 1;
else r = mid;
}
return l;
}
};
这个题目就是边界选择,而且这个题目的
check
是可以改变的,画个图理解l, r
的变化。