算法简介
二分法查找,也称为折半法,是一种在有序数组中查找特定元素的搜索算法。二分法分为整数二分和实数二分。整数二分涉及边界问题,实数二分不涉及边界问题。该算法讨论的是在整数上的二分。
算法思想
二分法的本质并不是具有单调性,它和单调性的关系是:如果具有单调性,则一定可以用二分法;如果没有单调性,也可以用二分法。那么二分的本质到底是什么呢?二分的本质是边界。假设我们给定了一个区间,我们在区间上定义了某种性质。在右半边区间是满足的,在左半边区间是不满足的。如下图:
如图所示,右边绿色的部分是满足该性质的,左边红色的部分是不能满足性质的。通过该性质可以将区间一分为二,一半满足该性质,一般不满足该性质。那么二分法可以寻找这个性质的边界,既可以寻找绿颜色的边界如b点,也可以寻找红颜色的边界如a点。具体步骤如下:
-
如果二分的是红色的点a
-
首先找到中间值mid=l+r+1 >> 1。
-
判断下该中间值是否满足某种性质,比如是否满足红色这部分性质。有两种情况:如果满足的话,说明mid一定在红色区间,那么答案是在区间[mid,r]中,mid这一点是可以取到边界点的,所以说mid可能是答案,包含mid。区间的更新方式是l=mid。当mid不满足红色性质的时候,那么mid一定满足绿色性质,mid一定在绿色的区间[l,mid-1]。区间的更新方式是r=mid-1。 此时区间被划分成[l,mid-1]和[mid,r],mid在右区间。
-
如果二分的是绿色的点b
-
首先找到中点mid=l+r>>1。
-
判断下该中间值是否满足某种性质,比如是否绿色这部分性质。有两种情况:如果满足的话,说明mid一定在绿色区间,那么答案是在区间[l,mid]中,mid这一点是可以取到边界点的,所以说mid可能是答案,包含mid。区间的更新方式是r=mid。当mid不满足绿色性质的时候,那么mid一定满足红色性质,mid一定在红颜色的区间[mid+1,r],不能取到mid。区间的更新方式是l=mid+1。 此时区间被划分成[l,mid]和[mid+1,r],mid在左区间。
模板
//区间[l,r]被划分成[l,mid]和[mid+1,r]时使用
int search_1(int l, int r){
while(l < r){
int mid = l + r >> 1;
if(check(mid)) r = mid; //check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
//区间[l,r]被划分成[l,mid-1]和[mid,r]
int search_2(int l, int r){
while(l < r){
int mid = l + r + 1 >> 1;
if(check(mid)) l = mid; //check()mid判断是否满足性质
else r = mid - 1;
}
return l;
}
选择模板
当一个二分问题出现的时候如何去考虑?
遇到问题的时候不需要考虑上面的图,即二分的到底是红色的点还是绿色的点。我们考虑的是每次先写一个mid,之后,写一个check()函数,判断下check()函数是true或者false的时候该如何更新区间。如果区间更新的是l=mid和r=mid-1时,mid要加一,反之则不需要不上加一。核心的地方就是每次更新区间的时候,看下是l=mid还是r=mid。如果l=mid,则mid需要加一,如果r=mid,mid不需要加一。然后根据对应的模板解题。
注意
当mid属于右边区间的时候,mid=l + r + 1 >>1进行向下取整(相加的整数做除法的时候时向下取整mid=l+r>>1),那么mid为什么要加上一呢?
假设l和r之间只差1,即l=r-1时。如果mid=l + r >> 1进行向下取整得mid=l。当check(mid)=true时,l=mid(mid在右半边时),l更新的时候,l还是等于mid,更新的区间没有改变,那么下次循环的时候区间也不会改变,就会发生死循环。因此在更新区间的时候mid需要加上1。先while循环一遍,mid=r,区间从[mid,r]变成[r,r],循环就会结束。
算法案例模拟
给定一个按照升序排列的长度为n的数组,以及q个查询。对于每个查询,返回一个元素k的起始位置和终止位置(位数从0开始计数)。如果数组中不存在该元素,则返回"-1 -1 "。
#include<iostream>
using namespace std;
const int N = 100010;
int n,m;
int q[N];
int main(){
//首先读入数组的长度和需要查询的数的个数
scanf_s("%d%d", &n, &m);
//循环读入
for (int i = 0; i < n; i++) scanf_s("%d", &q[i]);
//while控制查询的个数
while(m--){
int x;
scanf_s("%d", &x);
int l = 0, r = n - 1;
while (l < r){
int mid = l + r >> 1;
//开始二分左边的点
if (q[mid] >= x) r = mid;
else l = mid + 1;
}
//当我们数列中不存在x的话 二分出来的值就是从左往右看第一个大于x的值
if (q[l] != x) cout << "-1 -1" << endl;
else{
//输出l和输出r是一样的,因为当while循环结束的时候,l和r是相等的的
cout << l << ' ';
//开始二分右边的点
int l = 0, r = n - 1;
while (l < r){
int mid = l + r + 1 >> 1;
if (q[mid] <= x) l = mid;
else r = mid - 1;
}
cout << l << endl;
}
}
system("pause");
return 0;
}
上述的情况如下(q[mid]>=x和q[mid]<=x),如下图:
运行截图