二分查找是一种比较简单的题型,但是真的有那么简单吗?有必要深入探究吗?到底是记模板好还是理解好?
在我看来要二者兼备,我觉得二分查找原理是不难的,但是在细节上却不能出半点纰漏,不然就会输出错误值或者死循环。每个人的学习方法不同,理解了再记模板是比较好的,毕竟题目这么多,不可能每种情况都能用得上所谓的模板。
这里先给出二分查找最基本的两个条件(前两点)
比如某居民区停电了,检测出是某段电线出了问题,这时候电工需要对这段电线进行排查,找到精确的故障点。如果这段电线下有许多电线杆,那么电工就需要每个电线杆都爬上去操作一番才有可能排查出来故障点,过于麻烦。但是如果电工把这段电线分AB点,A是发电站方向B是居民区方向,一开始就排查区间[AB],找到AB中点C并进行排查,那么如果C没电说明故障在区间[AC],这时候按照相同方法,就可以找到精确的故障点,这种方法较为方便。
(上述括号仅表示括号,不表示开闭区间)
从上面的例子可以看出二分查找中正确找到二分查找的区间十分重要
通过电工的例子可以知道,二分法其实就是找到中间值,根据目标值和中间值的比较,从而去除一片搜索区域。在找数字的题目中,其实就是比较中间值mid和目标值target的大小
•中间值大于目标值 说明大于中间值的数全部也大于目标值,全部去除
•中间值小于目标值 说明小于中间值的数全部也小于目标值,全部去除
•如果相等,那就是找到了这个数字,跳出循环
根据这个思想就可以做出这类题(可以看我上一篇博客有讲解)
但是在写代码的过程中有几个点需要注意
① 区间的界定—[ left , right ]与[ left , right-1 ]
② 循环结束的条件----while(left<=right)与while(left③ 边界的界定----对于左闭右闭,左闭右开区间的理解
为什么都是讨论右边界没有左边界?文章最后会提到
为什么会有这几个易错点?拿电工的例子来说,对于点①,如果你把右边界(right)设置在最后一个元素和最后一个元素的后一位,是完全不一样的两种情况
如下图(元素代表电线杆)
第一种方法是把右下标right放在下标9,也就是元素10上,这时候如果是电线杆2(下标为1)出了故障,那么下标mid找到电线杆5(下标为4)时,是没电的,由于电线杆5没电,所以mid右边的,包括mid全部排除,把right移到mid的左边一格,也就是把mid-1赋值给right,此时right为元素4(下标为3),并且mid变成了元素2(下标为1)
----mid在我们自己计算时是(left+right)/2, 用下标计算,不能四舍五入,写代码的时候要写left+(right-left)/2防止溢出
----因为这种区间在每次的筛选中right也在筛选范围内,所以叫做左闭右闭区间,不理解的话可以对比后面的左闭右开区间
第二种方法是把right定义在元素11(下标10)上,这种做法的特点是在每次的筛选中right都不在筛选的范围内,这里我们暂时不去计算最后的结果,只计算两步来说明这种方法,仍然是电线杆2(下标为1)出了故障。第一次筛选时,mid下标为5,筛选范围是在[0,9]也就是[ left,right-1],说明不包含right
在筛选一次后,因为mid的下标5对应的元素6比元素2大,所以mid赋值为right(第一种是mid-1赋值为right)这便是和第一种方法不同的地方,可以看出在每次的循环中right都是不参与筛选范围的,所以取开区间
这种方法的核心就是right不参与筛选,所以mid本身被排除时,mid赋值给right,因此right一直循环都是不参与筛选的,从图可以看见right永远在区间的外面
区间分清楚了,还有另一个条件也是必须要搞清楚的
对比完区间,再看另一个易错点—循环条件,由于起始区间有区别,那么让整个循环停止下来的条件肯定也有区别
① while ( left <= right )
② while ( left < right )
可以看到它们的区别只是在于left等于right时能否再进行一次循环而已,所以这和前面的区间有何关联?
关联就是当你的区间取左闭右闭时,循环条件如果取① while ( left <= right )是可以正常运行的,包括里面没有这个元素,也是可以正确返回需要的值。
但是当你用② while ( left < right )时,就会出现元素还没找到时,就已经跳出循环了的情况
这里先给出一个易错点和重要的条件,left+(right-left)/2的计算必须写在循环内,而且是循环开始的时候。这样,每开始循环一次,mid的位置就更新一次
这里用一个简单的例子来说明,在一个有序整形数组arr [ 6 ] = { 1,2,3,4,5,6 }中寻找是否有数字4,即target=4
循环一次后,由于要找的元素4>arr[mid]=3,所以mid左侧包括mid全部舍去,即left来到了mid+1,也就是元素4,经过循环条件后mid来到了元素5
这时候由于arr[mid]=5>元素4,所以mid右边的数字包括mid全部舍去,right=mid-1,即right来到mid-1也就是元素4的位置,此时right与left处于同一个位置,这个时候本来在进行一次循环,就可以让mid指向元素4,就可以找到元素4了,但是由于条件是② while ( left < right ),而此时left==right,这时候进不去循环(上文有说mid的位置是在哪里更新的),自然mid也就找不到这个元素,从而输出了错误的答案
由此可知,双闭区间的循环条件是while(left<=right)
看完了第一种循环,再看第二种循环,这里就不再用相同的方法演示了,也是同样的例子,当区间是左闭右开时,②while(left
最后一个问题,为什么都是讨论右边界没有左边界?
我们可以定义一个数组,里面只有数字1,我们要查找的目标是target=5,(比1大即可),这时候采用左闭右闭,那么left,right都指向元素1,mid=(0+0)/2=0
这时候三个箭头都指向元素1,由于循环条件是while(left<=right),当left==right再运行一次,这时候满足了left的移动条件arr[mid] < target,执行如下判断
1.如果是 left = mid + 1;left会加一跳到下标1处,这时候left>right跳出循环,程序输出正常
2.如果是 left = mid;left、mid、right三者一直处于同一位置,由于mid不加,而且while(left<=right),left和right一直等于0,进入死循环。
如果采用左闭右开,那么left指向元素1,right指向元素2的位置
这时候mid=(0+1)/2=0,那么left与mid重叠,这时候满足了left的移动条件arr[mid] < target,进行如下判断
1.如果是 left = middle + 1; 这时 left = 0 + 1,left=right,会跳出循环,返回-1,程序正常。
2.如果是 left = middle;这是 left 仍然等于 0,left 依旧小于 right,不会跳出循环。而且每一次循环过后的数值都是:left = 0, right = 1
和初始条件 left = 0, right = 1 一模一样,这就是典型的死循环了。
这些就是我学习的总结了,码字不易感谢点赞关注支持!
附:两种运行代码
来源:leetcode.704.二分查找
第一种 左闭右闭、while(left<=right)
int search(int* nums, int numsSize, int target)
{
int left=0;
int right=numsSize-1; //左闭右闭
while(left<=right)
{
int mid=left+(right-left)/2;//防止溢出,并且放在循环内部
if(target>nums[mid])//要找的数太大,mid及其左边全部去除
left=mid+1;
else if(target<nums[mid])//要找的数太小,mid及其右边全部去除
right=mid-1; //right参与筛选
else
return mid; //刚好找到返回下标
}
return -1;//没找到返回-1
}
第二种 左闭右开、while(leftint search(int* nums, int numsSize, int target)
{
int left=0;
int right=numsSize; //左闭右开
while(left<right)
{
int mid=left+(right-left)/2;//防止溢出,并且放在循环内部
if(target>nums[mid])//要找的数太大,mid及其左边全部去除
left=mid+1;
else if(target<nums[mid])//要找的数太小,mid及其右边全部去除
right=mid; //right本身不参与筛选
else
return mid; //刚好找到返回下标
}
return -1;//没找到返回-1
}