分治算法:二分查找!昨天刚说不写算法了,但是突然想起来没写过分治算法的博客,所以强迫症的我……
STL函数库第五弹——二分函数lower_bound()、upper_bound()、binary_search()
由于笔者比较懒,所以把分治算法(二分查找篇)和STL第五弹放在一起。。。
Part 1:引入和导语
我们在做题的时候,经常会遇到一些需要分治的问题。(这是真的
今天的主角是——二分查找(开头提到过)。
二分查找,是针对于有序排列的数据调用而生的一种数据调用方法。
听上去很高端?我来讲个故事,大家肯定一下就会明白。
记得坐车的时候会收听电台,主持人有时会和观众玩这样一个游戏:
主持人在纸卡上写一个0-100的数字,要求听众在30秒内猜出这个数。
当听众说出一个数的时候,主持人会说出这个数相对于纸卡上的数是大了还是小了。
当然了,我相信主持人其实根本就没往纸卡上写数。
他也不会那么容易的让听众拿到奖品,所以每次猜一个数,他总会让听众尽可能少的排除错误答案。
比如你猜75,他会说“比75小”,这样你就只排除了25个错误答案。
那么怎么猜才是最聪明、最稳健的呢?
相信很多同学都已经想到了:猜50。这样就算主持人想阻挠拿到奖品,也可以一下子排除掉一半的错误答案。
那么猜完50呢?假如主持人说“比50大”,我们又该猜几呢?
对了——75,这样又能从剩下的里面排除一半错误答案。
——没错,这样每次缩小一半结果范围,不断缩小答案可能存在的区间,最终找到答案的方法,就叫做“二分法”。
如果我们使用二分法,在100个数中查找一个数,最坏的情况也只需要7次运算,而如果暴力枚举的话,可能就要几十次。
所以复杂度O(logn),而暴力的复杂度却是O(n)。
当然,使用二分法也有条件——数列必须有序,按照从小到大排序或者从大到小排序才可以。
(这里最好从小到大,笔者后面的代码也是按照从小到大来二分的)
Part 2:怎么写二分查找函数!
有一些加(丧)强(心)数(病)据(狂)的题目是这样的:
对于一个长度为n的不降序列,给出m组数据,请找出每个数在此序列中第一次出现的位置。
对于100%的数据,数据个数 n<=10^9;
如果暴力枚举的话,可爱的评测姬会非常不屑地回复你TLE三个字。(但是可以拿到部分分,唔,大概20~30分吧,不会太多的)
为了AC,我们就要用到二分,因为二分法的时间复杂度要优秀的多。
我们既然已经知道二分的原理了,剩下的就是怎么模拟二分了。
(鲁迅说过,算法来自生活实际,是模拟出来的。比如高精度算法,是模拟竖式……)
思路如下。
对于一个非常大的不降序列,注意必须是不降!!!我们有如下操作:
1、按照开头故事里的思路,我们要做出两个指针,分别指向这个序列的开头和末尾,表示二分到现在,答案是在这两个指针所指区间之内。
2、找出中间元素的值,然后比较它和所查找的那个数的值。如果比他大,那么排除比中间值小的数,把开头的指针(称为左指针)移动到中间值的位置;如果比他小,那么排除比中间值大的数,把末尾的指针(称为右指针)移动到中间值的位置。
3、重复步骤(2),直到找到该元素,返回元素下标。需要注意的是:如果左指针比右指针还要靠右(指向元素下标比右指针大),说明这个元素在此序列里不存在,这时,我们要退出循环二分查找。
具体代码如下,我已经竭尽一个蒟蒻所能的做了注释:
//这里指的左指针和右指针指向的是答案范围的最小值和最大值,初始时指向0和数组的最后一个元素
#include <cstdio>
#include <algorithm>
using namespace std;
int a[]= {,,,,,,,};//要查找的数组,二分前请确认不降序
int binarySearch(int x,int n)
{
int left =;
int right=n-;
while(left<=right)
{
int mid=(left+right)/;//int类型除法,不考虑小数(求中间的元素)
if(x==a[mid])
{
return mid;//如果这个中间值就是要找的元素,返回值
}
if(x>a[mid])//如果比他大,左指针指向中间值的下一个元素
{
left=mid+;
}
else//没有他大,右指针指向中间值的前一个元素
{
right=mid-;
}
}
return -;//未找到x
}
//二分递归实现
int recurisonBinarySearch(int left,int right,int x)
{
if(left>right)
{
return -;//左指针比右指针大,但是还没有找到答案,说明没有此元素,返回-1
}
int mid=(left+right)/;//求中间元素位置
if(x==a[mid])
{
return mid;//如果这个中间值就是要找的元素,返回值
}
if(x>a[mid])//比他大,中间值当做左指针,进行递归二分查找
{
return recurisonBinarySearch(mid+,right,x);
}
else//没他大,中间值当做右指针,进行递归二分查找
{
return recurisonBinarySearch(left,mid-,x);
}
}
int main()
{
int x;
int ans1,ans2;
scanf("%d",&x);//输入要查找的数
ans1=binarySearch(x,);//进行循环二分查找
ans2=recurisonBinarySearch(,,x);//进行递归二分查找
printf("%d %d\n",ans1,ans2);//输出元素在数组中的位置
return ;
}
这里还有一个要点,就是我这么写二分,只能对升序序列生效,如果序列中有多个相同的值,这个函数返回哪一个的下标我就不知道了。
如果题目中有要求,返回第一次出现的下标,我们需要手动在函数里加一个循环,找到该元素第一次出现的位置。
如果需要查找最接近某元素的值,二分到最后,确定该元素不在序列中,我们可以比较左指针所指元素的前一个或者后一个元素和左指针所指的元素,看看哪一个更接近即可。
这些只需要对函数做一些小小的修改,都很好写,笔者就不多BB了。
Part 3:STL二分函数
终于到了STL函数库出场的时间了!
伟大的#include<algorithm>!
为了方便我们这些蒟蒻编程和dalao们压缩代码,减少工作量,神犇们创造了这三个函数:
lower_bound()、upper_bound()、binary_search()
我们来一位一位介绍。
首先,binary_search()函数,作用:对一个不降序列进行二分查找,如果所查找的值在序列中出现了,返回true,没有出现,返回false。
然后,lower_bound()函数,作用:对一个不降序列进行二分查找,返回第一个大于等于所查找的值的元素下标,注意返回的是指针变量!!!
最后,upper_bound()函数,作用:对一个不降序列进行二分查找,返回第一个大于所查找的值的元素下标,注意返回的是指针变量!!!
这么方便的函数怎么用?
我们设置以下变量:
num[ ](需要被查找的序列);
lenth(序列长度);
x(需要查找的元素);
那么这几个函数使用的格式大概是这样的:
binary_search(num,num+lenth,x);
lower_bound(num,num+lenth,x);
upper_bound(num,num+lenth,x);
放到具体代码里是这样的:
#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
int a[]={,,,,,,,};
int b=binary_search(a,a+,);//查找成功,返回1
cout<<"在数组中查找元素4,结果为:"<<b<<endl;
int c=binary_search(a,a+,);//查找失败,返回0
cout<<"在数组中查找元素40,结果为:"<<c<<endl;
int d=lower_bound(a,a+,)-a;
cout<<"在数组中查找第一个大于等于10的元素位置,结果为:"<<d<<endl;
int e=lower_bound(a,a+,)-a;
cout<<"在数组中查找第一个大于等于100的元素位置,结果为:"<<e<<endl;
int f=upper_bound(a,a+,)-a;
cout<<"在数组中查找第一个大于10的元素位置,结果为:"<<f<<endl;
int g=upper_bound(a,a+,)-a;
cout<<"在数组中查找第一个大于101的元素位置,结果为:"<<g<<endl;
}
需要注意的是,因为这lower_bound()和upper_bound()这两个函数返回的值是指针变量,所以要把它变成int类型,要减去数组名。
ps:如果你所查找的数值比序列里最大的那个数还要大,lower_bound()和upper_bound()这两个函数返回的下标是lenth+1,注意这个东西有可能是越界的,所以不想RE的话就稍微处理一下吧!!!
Part 4:实战演练
由于笔者今天刚刚粗略的学了学STL大法,还不是很熟练,所以做题不多(只有一道)望谅解,有时间的话会专门发一篇二分查找的题解。
这个题,因为给出的序列已经是非降序列了,所以不需要先sort()来排序。
思路很简单——lower_bound()查找,需要注意的是比较的时候,不要越界。
先上加注释的代码,相信大家一看就懂。、
//1240
//#include<zhangtao.std>
#include<algorithm>//别忘了头文件
#include<cstdio>
using namespace std;
int n1,n2,a,b,num[];
int main()
{
scanf("%d",&n1);//n1为序列长度
for(int i=;i<n1;i++)
{
scanf("%d",&num[i]);//输入序列
}
scanf("%d",&n2);//输入询问次数
for(int i=;i<n2;i++)
{
scanf("%d",&a);//输入查询数据
b=lower_bound(num,num+n1,a)-num;//二分函数查找
if(b-<)//先特判,保证不要越界
{
//如果查找的数比数列中第一个数还要小,直接返回第一个元素即可
printf("%d\n",num[b]);
continue;
}
if(b>=n1)
{
//这里特判的是查找的数比数列中最大的数还要大的情况
//这时函数返回b的值为序列长度+1,我们返回序列的最后一个元素即可
printf("%d\n",num[b-]);
continue;
}
if(abs(num[b-]-a)<=num[b]-a)//题目要求如果一样接近,返回较小值,所以是<=
{
//元素num[b]>=a,num[b-1]<a判断哪个更接近一些,由于前面已经特判过越界的情况,可以放心比较
printf("%d\n",num[b-]);
continue;
}
else printf("%d\n",num[b]);
}
return ;//return 0好习惯
}
好了,今天的学习分享就到这里了,最后BB一句,能用STL的,我们绝对不要手写二分,不仅容易写错,而且费时间(蒟蒻思维)
记住这三个函数!!!lower_bound()、upper_bound()、binary_search(),非常方便,非常方便,非常方便!!!
老规矩——祝大家,身(少)体(掉)健(头)康(发)。