二分查找
前言
本文目前只对整数二分进行入门式探讨。
什么是二分查找
二分查找(Binary Search)是一种在有序数组中查找特定元素的算法。它的原理是将目标值与数组的中间元素进行比较,如果相等,则返回该元素的索引;如果目标值小于中间元素,则在数组的左半部分继续查找;如果目标值大于中间元素,则在数组的右半部分继续查找。通过每次将查找范围缩小一半,最终可以快速地找到目标值或确定其不存在。
二分查找的前提条件是数组必须是有序的,可以是升序或降序。这种算法的时间复杂度为O(log n),其中n是数组的大小。相比于线性查找等其他方法,二分查找通常具有更高的效率。
二分查找的适用条件
那什么时候适用二分查找呢?
既然二分查找是每次通过比较中间元素来确定下一步查找的范围,那显然该组元素必须是有序的,即我们将待比较的该数组元素分为左端点、右端点、中间元素,如果中间元素小于我们的答案期望时,此时最佳元素一定落在中间元素到右端点这段区间,否则就是左端点到中间元素这段区间。那如果该组元素是无序的呢,我们通过比较中间元素然后缩小查找范围显然是不通逻辑的,例如一列无序的不重复的随机数,查找100时,中间元素是150时我们下一步就会检索左端点至中间元素这段范围,显然,这时100究竟在左半段区间还是右半段区间我们是无法确定的。
二分的两种情况
现在我们用l表示左端点,mid表示中间元素,r表示右端点。数组为单调不递减数组。
情况一:
while (l < r)
{
int mid = (l + r)/ 2;
if (check(mid)) r = mid;
else l = mid + 1;
}
check函数即是与目标元素进行比较,可以发现该情况当mid符合目标元素时,将下次待检索范围定在了l~~~mid,不符合目标元素时,下次就会检索mid+1~~r;这种情况呢就是寻找一个尽可能小的期望答案,因为我们每次都尽可能往左边去寻找。
然而初学者在使用二分时经常会出现while死循环的问题,就是l和r一直会处在r=l+1的状态,符合l<r的while条件会一直处于该循环内,造成死循环。
那让我们来看看该情况时如何避免死循环的。
因为此时我们是寻找一个尽可能小的期望答案,当最终检索范围来到l~~l+1时,我们肯定是优先去check l 是不是我们所需要的那个答案,如果 l 符合,执行 r = l;符合我们的需求,那如果我们不去 check l 而是 check l+1 呢,这时最终结果就会直接变成了l + 1然后不会去检索 l 导致错过了我们真正所需要的答案。
所以该情况的mid我们使用整除来避免发生上述情况,因为整除的特殊性,当每次来到 l 和 l+1这种情况,mid运算都会得到 l ,这样就能避免了发生上述情况。
实例:
在 1 2 3 4 5 6 7 8 9 10寻找第一个大于等于6的数,也就是寻找一个尽可能小的大于等于6的数
第一次循环 l = 1 r = 10 mid = 5,if语句不成立,此时l = mid + 1 =6;
第二次循环 l = 6 r = 10 mid = 8 ,if语句成立,此时r = mid =8
第三次循环 l = 6 r = 8 mid = 7, if语句成立, 此时r = mid = 7
第四次循环 l =6 r = 7 mid = 6, if语句成立, 此时r = mid =6,退出循环,此时l = r =6,即找到期望答案6。
情况二:
while (l < r)
{
int mid = (l + r + 1)/2;
if (check(mid)) l = mid;
else r = mid - 1;
}
情况一是寻找一个尽可能小的期望答案,那本情况自然是寻找一个尽可能大的期望答案了。同情况一一样,我们所需要处理的就是l 和 l+1的这种最终情况,当寻找尽可能小的值我们要优先check l,那寻找尽可能大的值我们就要优先check l + 1了。那怎么让l 和 l+1的情况每次都让mid变成l + 1呢,我们可以让mid = (l + r +1)/2,这样就能让mid每次都来到l+1。
实例:
在 1 2 3 4 5 6 7 8 9 10寻找最后一个小于等于5的数,也就是寻找一个尽可能大的小于等于5的数
第一次循环 l = 1 r = 10 mid = 6,if语句不成立,此时r = mid - 1 =5;
第二次循环 l = 1 r = 5 mid = 3 ,if语句成立,此时l = mid =3
第三次循环 l = 3 r = 5 mid = 4, if语句成立, 此时l = mid = 4
第四次循环 l =4 r = 5 mid = 5, if语句成立, 此时l = mid =5,退出循环,此时l = r =5,即找到期望答案5。
二分查找实战
这样看起来二分查找实用性不大,其实不然,当需求是求出最大值或最小值的时候往往能用二分查找来实现。
例如这道题:
题目地址点击这里
来源:牛客网
有 n 棵树,初始时每棵树的高度为 Hi,第 i 棵树每月都会长高 Ai。现在有个木料长度总量为 S 的订单,客户要求每块木料的长度不能小于L,而且木料必须是整棵树(即不能为树的一部分)。现在问你最少需要等多少个月才能满足订单。
通过观察,题目要求出现了“最少”这个字眼,进一步思考,假设现在我们10个月能满足客户订单,那么11个月也一定可以满足客户订单,那这是不是就是二分查找所要求的单调性呢?
显然是的,这样我们就可以套用二分查找来实现这道题目了。
因为答案是月数,也就是整数,为了不错过“最少”的这个答案,二分查找所处理的有序数组就是以1开始,公差为1的一组等差数列。而末项设为多少就视题目数据范围而定了,我们只需要设定一个无论题目给出的数据是多少,该数一定能满足订单就够了。
而check函数就是把mid这个月数代入进去,把所有经过mid个月的生长后高度大于等于L的树的高度累加起来,如果总量大于等于S,return true,否则return false。
更多的二分例题都可以在各大知名刷题网站上找到,本文如有表述不清有歧义的可以在评论区讨论,欢迎各位不吝赐教。