摘要
优点:逻辑简单、易实现。
难点:边界问题易混淆。
核心:搜索区间的定义及收缩。
最近遇到一个二分搜索问题,本以为比较简单,但是几次尝试,要么搜索结果错误,要么就是死循环,哎,还是小看到这个问题了。
经过分析,问题主要出在边界处理问题上。在业务开发中边界问题往往是引发BUG的一个重灾区,在二分法中也不例外,leetcode上还总结出了相应的模板,可见踩坑之多。
其实只要搞清楚核心问题,随时手撕代码。拒绝背模板,背代码(记忆超人请忽略…)。
图片
二分法的关键点
1、搜索区间的边界定义。
2、搜索区间的收缩。
3、搜索结束的条件。
4、搜索结束后,区间的两个指针位于什么位置。
场景一:递增数组,查找目标值的位置
我们根据下面的示例,围绕核心问题:“搜索区间不为空”,来依次查看几个关键点:
1、定义搜索区间,也就是定义边界:
该示例中采用左闭右开[left,right)。为什么选择左闭右开?个人习惯,你也可以选择左闭右闭[left,right],关键是明确搜索区间边界。
2、搜索区间的收缩:
区间指针left,right在每一轮搜索后,都要进行调整,这也是造成死循环的一个潜在问题。
1)、调整基点:基于中位数进行调整。
2)、调整方向:趋近于目标值的方向。
3、搜素失败的条件:
在[left,right)区间中,为了保持区间有效,必须保证额left<right,否则区间是无效的。
4、指针的位置:
假如未找到目标值,此时left=right,区间缩减为0,nums[left]是第一个大于target的数值。
/*
nums:递增数组
numSize: 数组长度
target:搜索的目标值
return: 目标值在数组中的索引,如果未找到,返回-1
*/
int search(int* nums, int numsSize, int target) {
if( nums == NULL || numSize == 0 ){
return -1;
}
/*
1、初始化搜索区间
2、采用左闭右开[left,right)
3、left指向区间的左起始位置,
right指向区间的结束位置
*/
int left=0,right=numsSize;
/*
1、循环结束条件
2、因为采用左闭右开
1)当left=right时,
区间长度为0,搜索结束
2)当left>right时,
区间长度为负,没有意义,搜索结束
3)当left<right时,
区间长度不为空,继续搜索
*/
while( left<right ){
//区间长度
int len = right-left;
//区间中位索引
int mid = len/2 + left;
if( nums[mid] == target ){
return mid;
}
//[left,mid-1] mid [mid+1,right)
if( target < nums[mid] ){
/*
1、目标值在区间左半边,向左收缩区间,
right左移
2、当len=2时,mid=left+1,
right左移后,区间长度缩小为1
3、当len=1时,mid=left,
right左移后,区间缩小为0
*/
right=mid;
}else{
/*
1、目标值在区间的右半侧,向右收缩区间,
left右移
2、当len=2时,mid=left+1,left右移后,
left==right,区间长度缩小为0
3、当len=1时,mid=left,left右移后,
left==right,区间长度缩小为0
*/
left=mid +1 ;
}
}
assert( left == right );
return -1;
}
场景二:求平方根,结果向下取整
下面的例子,我们仍然围绕核心:“搜索区间不为空”,并查看几个关键点:
1、定义搜索区间:
上个示例中,采用左闭右开[left,right),为了展示不同用法,这次我们可以选择左闭右闭区间[left,right],进一步明确区间的定义不用一成不变,关键是明确搜索区间边界 。
2、搜索区间的收缩:
和上一个示例一致,不再赘述。
3、搜索失败的条件:
在[left,right]区间中,为了保持区间有效,必须保证left<=right,否则区间是无效的。
4、指针的位置:
假如未找到目标值,此时left>right,nums[right]的平方时最接近,且不会超过目标值。
/*
求x的平方根,向下取整
*/
int mySqrt(int x) {
/*
1、初始化搜索区间
2、采用左闭右闭[left,right]
3、left指向区间的左起始位置,right指向区间的最后一个有效数值。
*/
int64_t left=0,right=x;
/*
1、循环结束条件
2、因为采用左闭右闭
1)当left<=right时,区间长度不为空,继续搜索
2)当left>right时,区间长度为负,没有意义
*/
while( left<=right ){
int64_t len = right-left+1; //区间长度
int64_t mid = len/2 + left; //中位数值
int64_t x1 = mid * mid; //中位数平方
if( x == x1 ){
return mid;
}
//[left,mid-1] mid [mid+1,right]
if( x1 > x ){
/*
1、mid平方x1大于目标值,
搜索区间需要向左收缩
2、当len==2时,
mid == left+1 == right,
收缩后,区间长度为1
3、当len==1时,
mid == left == right,
收缩后,循环退出,
1)[right,left] -> right跨过left,来到了left的左边。
2)right的平方是最大且不超过目标值的整数。
3)left的平方是最小且超过目标值的整数。
*/
right=mid-1;
}else{
/*
1、mid平方x1小于目标值,
搜索区间需要向右收缩
2、当len==2时,
mid == left+1 == right,
收缩后,区间长度为1
3、当len==1时,
mid == left == right,
收缩后,循环退出。
1)[right,left] -> left 跨过right,来到了right的左边。
2)right的平方是最大且不超过目标值的整数。
3)left的平方是最小且超过目标值的整数。
*/
left=mid+1;
}
}
return right;
}
手敲一遍,胜过默背十遍。