二分算法原理比较简单,但是实际的算法模板却有很多,这一切都源于二分查找问题中的复杂情况和二分算法的边界处理,以下是博主对一些二分算法查找的情况分析。
需要说明的是,以下二分算法都是基于有序序列为升序有序的情况,如果针对有序序列为降序的情况,二分的原理是相同的,将二分中用于check的条件表达式取反即可(小于变大于,小于等于变大于等于).
文章目录
- 1. 模板1
- 2.模板2
- 2.1 边界死循环问题
- 3. 总结
1. 模板1
int search(vector<int>& nums, int target) {
int l = 0,r = nums.size() - 1;
while(l <= r)
{
int mid = (l + r) >> 1;
if(nums[mid] == target) return mid;
else if(nums[mid] > target) r = mid - 1;
else l = mid + 1;
}
return -1;
}
上述这种二分算法是较为常见的,可以用以查找某个数是否在一个序列中,如果在,就返回相应的下标;如果不在,就返回-1。
但是这种二分算法对于目标数有多个的情况,无法准确定位到位于左右边界的目标数,而且对于不存在目标数的序列,上述算法并不能找到第一个大于目标数或第一个小于目标数的下标。
2.模板2
为了解决第一类模板中无法解决的问题,第二类二分算法模板做出了改进。
对于序列中目标数存在多个的情况,这类模板实际提供两套模板,分别对应查找右边界和左边界。
int find_left(vector<int>& nums,int target)
{
int l = 0,r = nums.size() - 1;
while(l < r)
{
int mid = (l + r) >> 1;
if(nums[mid] >= target)
r = mid;
else
l = mid + 1;
}
return l;
}
int find_right(vector<int>& nums,int target)
{
int l = 0,r = nums.size() - 1;
while(l < r)
{
int mid = (l + r + 1) >> 1;
if(nums[mid] <= target)
l = mid;
else
r = mid - 1;
}
return l;
}
上述两种二分算法分别能够定位到位于左右边界的目标数,且能够处理好边界问题,而不陷入死循环。
而对于目标数不在序列中的情况,上述两种算法也能很好处理。对于左边界的情况,它会找到当前序列中最接近目标数的位置(在将目标数按照顺序插入原序列的情况下),且优先找大于目标数的位置,即优先找原序列第一个大于目标数的数;对于右边界的情况,它与左边界时的查找逻辑相同,唯一的区别在于它会优先找小于目标数的位置,即优先找原序列第一个小于目标数的数。
2.1 边界死循环问题
首先,我们要知道为什么二分算法会出现边界死循环问题。
对于整数范围中的二分算法,由于除法运算的向下取整特性,因此,当两个相邻的数相加再除以2后,总会得到这二者中较小的数,在特殊的情况下,会导致更新后,用于下一次二分的两边界与上一次相同,因此造成死循环。
在上述呈现的分别找左右边界的算法模板中,取中值mid
时,有是否加上1的区别,这实际上也是为了处理边界死循环问题。
对于找左边界的情况,我们只考虑最后的临界情况,即左边界l
与右边界r
相邻的情况(因为只有在这种情况下,才可能出现边界死循环问题)。
当l
与r
指向的数不相同时:
如果要找到的下标是l
,那么此时一定会执行r = mid
这条语句,而mid == l
,所以这种情况不会陷入死循环。
如果要找到的下标是r
,那么此时一定会执行l = mid + 1
这条语句,而mid == l
且r = mid + 1
,因此也不会陷入死循环。
当l
与r
指向的数相同时:
由于是找左边界,所以一定会执行r = mid
这条语句,所以最终也不会陷入死循环。
对于找右边界的情况,我们同样只考虑最后的临界情况,即l
与r
相邻的情况。
当l
与r
指向的数不相同时:
如果要找的下标是l
,由于mid = (l + r + 1) / 2
,因此此时mid
实际为r
,那么此时一定会执行r = mid - 1
这条语句,所以不会陷入死循环。
如果要找的下标是r
,mid
为r
,因此实际会执行l = mid
,也就是将l
变为r
,所以不会陷入死循环。
当l
与r
指向的数相同时:
由于是找右边界的情况,所以会执行l = mid
这条语句,而由于找右边界中,mid额外做了加1处理
,所以mid == r
,因此最终l
就变为r
,从而找到右边界,不会陷入死循环。
所以在找右边界中,对mid
所做的加1处理,是很巧妙的处理——既解决了二分找右边界过程中会出现的死循环问题,又未破坏算法的整体逻辑。
3. 总结
综合来看,第二类二分算法模板适用范围更广,能很好地应对各种二分查找的情况,且不会出现边界死循环问题,因此第二类二分算法中的两个模板更推荐使用。