这是我参与8月更文挑战的第1天,活动详情查看:8月更文挑战
二分法是一项非常实用的算法技巧,通过二分法可以将查找数据的复杂度降低到对数级别。这篇文章是我自己学习二分法时记录下来的一些个人思考和细节探究,希望可以帮助到大家。
前提条件
数组必须是单调(可以是非严格)的,也就是已经排好序了,否则是无法使用二分法的。 下文中的所有例子都假设数组已经从小到大排序好了,从大到小其实就是反过来推理。
区间的选择
二分法的左右边界选择一共有4种,左右边界都是开区间(左开右开),左边和右边都是闭区间(左闭右闭),左边界为开区间右边界为闭区间(左开右闭),左边界为闭区间右边界为开区间(左闭右开),一般来说,为了不让left为负数影响观感,左边界一般选择闭区间,在左闭右开和左闭右闭中,又以左闭右开为主流。
如何得出mid
无论是左闭右开还是左闭右闭,得出mid的公式都为
int mid=(left+right)/2
//可能会溢出
这样求出来的mid始终保证比right小(左闭右开的情况下,左闭右闭则小于等于right),不会越界,不用多余的判断
如果担心溢出的情况,可以把公式改进为
int mid=left+((right-left)>>1)
何时结束循环
如果在循环过程中直接找到了数,那么可以提前结束循环,如果始终没办法找到,这取决于区间的选择是左闭右闭还是左闭右开。无论如何,左右边界所囊括的区域应该没有任何元素,以保障数组中的所有元素都被排查。
如果是左闭右开,那么循环条件应该为
while(left<right)
{...}
结束循环时,left==right
如果是左闭右闭,那么循环条件应该为
while(left<=right)
{...}
结束循环时,left==right+1
怎么改变区间
当mid下标对应的值就是我们要找的数的时候,自然可以直接返回。否则的话就要排除掉不可能存在目标的区间,缩小排查范围。下面给出左闭右开和左闭右闭两种情况下得区间变动
左闭右开
当nums[mid]<target时,小于mid下标的值都可以不用看了,因为它们也一定比target更小,mid下标本身也小于target也应该丢弃。所以我们要挪动左边界。
left=mid+1;
打个比方,下标3的值已经小于target了,我们接下来的排查区间应该从剩余可能的区间开始,左闭右开下这个值应该是[4,right)
同理,nums[mid]>target时,大于mid下标的值都可以不用看了,因为它们也一定比target更大,mid下标本身也大于target也应该丢弃。所以我们要挪动右边界。
right=mid;
还是下标为3的例子,3号下标的值已经比target大了,那么剩余的区间就是从begin到2都可以选择,左闭右开下,就是[0,3)
一个“二分法"找数字的模板如下(左闭右开)
while(left<right)
{
int mid=left+((right-left)>>1);
if(nums[mid]==target)
{
return mid;
}
if(nums[mid]<target)
{
left=mid+1;
}
else
{
right=mid;
}
}
return -1;
左闭右闭
左闭右闭的道理其实是一样的,只需要记得把包括mid在内的已经不可能包含目标值的区间通通舍弃,再代入到左闭右闭的区间中就可以轻松地推理出left和right的变化
while(left<=right)
{
int mid=left+((right-left)>>1);
if(nums[mid]==target)
{
return mid;
}
if(nums[mid]<target)
{
left=mid+1;
}
else
{
right=mid-1;
}
}
return -1;