以下内容整理和参考一些博客,还有和自己的理解,若有错误请不吝指出
二分查找是针对有序数组所用的一种快速查找元素的方法。
二分查找的条件以及优缺点
条件:针对有序数组(元素从小到大或从大到小)
优点:查询速度较快,时间复杂度为O(log2n)
缺点:有硬性条件的限制,而且即使查到后,插入与删除困难。
二分查找一般有三个应用场景:①查找一个数②寻找左侧边界③寻找右侧边界
大致模板如下:
int Binary_Search(int num[], int key) {
int left = 0, right = ...;
while(...) {
int mid = left + ((right - left) >> 1); //防止溢出,+的运算优先级高于>>,所以要记得把后面的加上括号
if (num[mid] == key) {
...
}
else if (num[mid] < key) {
left = ...
}
else if (num[mid] > key) {
right = ...
}
}
return ...;
}
搜索区间为 [left, right] 则 while (left <= right), left = mid + 1, right = mid - 1.
搜索区间为 [left, right) 则 while (left < right), left = mid + 1, right = mid.
①二分查找一个数
查找一个数,找到则返回其索引,否则返回-1。(该算法的局限性:当是在一组有序序列中查找存在重复的数时,返回的索引只是该数其中的一个,不一定是第一个,即该算法无法处理寻找这个数的左右边界)
int Binary_Search(int num[], int key) {
int left = 0, right = num.length - 1;
while(left <= right) {
int mid = left + ((right - left) >> 1);
if (num[mid] == key) return mid;
else if (num[mid] < key) left = mid + 1;
else if (num[mid] > key) right = mid - 1;
}
return -1;
}
/*折半查找,递归实现*/
int Binary_Search2(int *a, int low, int high, int key)
{
if(low > hign) return -1;
int mid = (low + high) / 2;
if(a[mid] == key) return mid;
if(a[mid] > key) return Binary_Search2(a, low, mid-1, key); //有没有return都可以。
else return Binary_Search2(a, mid+1, high, key); //有没有return都可以。
}
②寻找一个数左边界的二分查找
可用于查找第一个大于或等于key的数的索引位置,相当于lower_bound函数。
当 num[mid] == key (即查找到key)时不要立即返回,而是缩小搜索区间的右界right,在[left,right)中继续查找,使得区间不断向左收缩,达到锁定左侧边界的目的
当循环终止时left == right,right == mid,此时mid为第一个key的索引(即左侧边界),所以key的左边界索引为left或right。
int left_bound(int num[], int key) {
if (num.length == 0) return -1;
int left = 0, right = num.length;
while (left < right) {
int mid = left + ((right - left) >> 1);
if (num[mid] == key) right = mid;
else if (num[mid] < key) left = mid + 1;
else if (num[mid] > key) right = mid;
}
/*return left; 返回第一个大于或等于key的数的索引位置 */
return num[left] == key ? left : -1; //key存在则返回左边界索引,不存在返回-1
}
②寻找一个数右边界的二分查找
可用于查找第一个大于key的数的索引位置,相当于upper_bound函数。
当 num[mid] == key (即查找到key)时不要立即返回,而是增大搜索区间的左界 left,在[left,right)中继续查找,使得区间不断向右收缩,达到锁定右侧边界的目的。
注意,循环终止时left=mid+1,所以此时mid是最后一个key的位置(即右侧边界),故mid=left-1,所以key的右边界索引为left-1。
int right_bound(int num[], int key) {
if (num.length == 0) return -1;
int left = 0, right = num.length;
while (left < right) {
int mid = left + ((right - left) >> 1);
if (num[mid] == key) left = mid + 1;
else if (num[mid] < key) left = mid + 1;
else if (num[mid] > key) right = mid;
}
/*return left; 返回第一个大于key的数的索引位置,这里left不用-1*/
return num[left-1] == key ? left-1 : -1; //key存在则返回右边界索引,不存在返回-1
}
优化:
mid = (left+right)/2或middle= (left+right)>>1; 这样的话left与right的值比较大的时候,其和可能溢出。所以可以用int mid = left - (left - right) /2;
或middle=left + ((right-left)>>1);
来防止越界,或用int mid = left & right + (left ^ right) >> 1;
(进行计算机最喜欢的位运算,效率略高)来代替求两个数的平均值的操作。
补充:
二分的难点在于最后的边界问题,处理不好就会陷入死循环。有一种很稳妥的方法能很好的规避这些边界问题:我们可以二分到区间小到一定程度,比如5个以下,然后去顺序查找。这样处理对于二分的时间复杂度影响也极小。
如下代码所示。
int r=1000000000,l=0,ans;
while(r-l>4){
int mid=l+((r-l)>>1);
if(check(mid))l=mid;
else r=mid-1;
}
for(int i=l;i<=r;i++){
if(check(i))ans=i;
}
C++STL中的二分查找函数:
STL中与二分查找相关的函数有4个,分别是lower_bound
, upper_bound
, equal_range
和binary_search
。
其中每个函数实现的功能如下:
- binary_search:查找某个元素是否出现,返回bool型。
- lower_bound:查找第一个大于或等于某个元素的位置。
- upper_bound:查找第一个大于某个元素的位置。
- equal_range:查找某个元素出现的起止位置。注意,终止位置为最后一次出现的位置加一。(返回一个pari对象,pair::first是指向子范围左边界的迭代器. pair::second是指向子范围右边界的迭代器.它们的值和lower_bound,upper_bound分别返回的值相同.)
关于lower_bound和upper_bound:
这两个函数的返回值都是地址或迭代器(也就是标准库中的容器iterator)。
我们也可以用其返回值减去数组首地址得到我们习惯的数组下标。
当查找的元素一次都未出现时,二者返回的结果都是第一个大于该元素的位置。
int b[7]={1,2,3,3,3,4,6};
int k1=*upper_bound(b,b+7,3);//输出k1为值4
int *k2=upper_bound(b,b+7,3);//输出k2为地址0x6dfee0,输出*k2为值4
auto k3=upper_bound(b,b+7,3);//输出k3为地址0x6dfee0,输出*k3为值4
int k4=upper_bound(b,b+7,3)-b;//输出k4为下标5
/*lower_bound用法也一样*/
若要查找一个有序数组b中某个值key的个数,可以用以下代码求出:
int t=upper_bound(b,b+n,key)-lower_bound(b,b+n,key);
上述的二分查找函数的使用情况是默认从小到大的有序(即升序),如果要在从大到小的有序序列(即降序)使用时,我们需要使用函数的第4个参数去重载,第4个参数类似sort的第3个参数。
bool cmp(int x,int y){return x>y;}
int a[7]={6,5,4,4,3,2,1};
int k1=lower_bound(a,a+7,4,greater<int>())-a;//输出为下标2
int k2=upper_bound(a,a+7,4,greater<int>())-a;//输出为下标4
int k3=upper_bound(a,a+7,4,cmp)-a;//输出为下标4
STL库二分函数的代码应用如下:(题目文章开头已贴出)
#include<bits/stdc++.h>
using namespace std;
int main()
{
int M,a[200005],n,x;
scanf("%d",&M);
for(int i=0;i<M;i++)scanf("%d",&a[i]);
scanf("%d",&n);
for(int j=0;j<n;j++){
scanf("%d",&x);
int t=binary_search(a,a+M,x);///该二分查找函数为bool型,判断查找的数是否存在
int *k=lower_bound(a,a+M,x);///找到需要查找的数的第一个位置(为地址)
int *q=upper_bound(a,a+M,x);///找到比需要查找的数大的第一个数的第一个位置(为地址)
int p=q-a-(k-a);///q-a和k-a分别得出两者的数组下标
if(t){
if(p==1) ///数组中无重复数字x的情况
printf("%d",k-a);
else ///数组中有重复数字x的情况
for(int w=k-a;w<q-a;w++)printf("%d ",w);
}
else printf("Not Found");
puts("");
}
return 0;
}
除了上述整数的二分外,还有针对浮点数的二分。
浮点数二分算法
浮点数二分相比于整数的二分好写很多,不用考虑边界问题。
浮点数二分有两种写法 :
- 以循环次数为循环终止条件
- 以精度为循环终止条件
第二种写法如果eps太小, 会可能由于浮点数的精度问题陷入死循环, 所以推荐第一种写法
for(int i=0; i<60; ++i){ //i为循环次数, 60次循环可以让精度达到初始区间长度的1/1e18
double mid=(l+r)/2.0;
if(check(mid))l=mid;
else r=mid;
}
const double eps=1e-8;
while(r-l<eps){ //其中eps是所需要的精度
double mid=(l+r)/2.0;
if(check(mid))l=mid;
else r=mid;
}