当题目循环次数超过了亿级,再用暴力枚举就很有可能超时了,此时,我们就要请出大名鼎鼎的二分了!
二分查找也称折半查找,它是一种效率极高的查找方法。但是折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列,就是:数据要是有序排列的。备注:二分查找的一个非常重要的前提条件就是查找的内容具备单调性。举个例子假如我们需要在10亿个不同的数字当中找到目标数字那么通过以往学习的知识点我们通过循环查找那么必定会超时,如果我们使用二分查找这种方式的话那么就不会出现超时的情况了。+我们可以先将这些不同的数字排好序,然后找到中间的那个数,如果中间的数字比目标数字大那么我们就可以排除从中间到末尾的所有数字,反之我们可以排除从开头到中间的所有说,每次都按照此方法比较排除那么我们最多只需要30次(10亿介于2的29次方和2的30次方之间)就可以完成该目标的查找了,这样极大的提升了程序的效率。再来一个现在有一堆硬币中混入了一枚假硬币,从外观上不能区分真假硬币,但是由于真假硬币的材质不一样,所以假硬币要轻一点点,用手无法直接区分。现在给你一个天平,请问要怎样找出假硬币?思路:分两种情况讨论,第一种情况,奇数个的硬币:我们首先拿出一个硬币,把剩余的硬币进行平分,放在天平的两端,如果天平此时平衡,那我们手中拿的那枚硬币就是假硬币。如果天平倾斜,假的硬币比真的硬币轻一点点,所以说天平会往轻的那边倾斜,假的硬币就在轻的一边。我们接着把有假硬币的这些硬币,再进行平分,放在天平上称就可以了。第二种情况:偶数个硬币,直接平分就可以。二分查找算法思想对于n个有序且没有重复的元素(假设为升序),从中查找特定的某个元素x。我们可以将有序序列分成规模大致相等的两部分,然后取中间元素与要查找的元素x进行比较,如果x等于中间元素,查找成功算法终止%3B如果x小于中间元素,则在序列的前半部分继续查找,否则在序列的后半部分继续查找。这样就可以将查找的范围缩小一半,然后在剩余的一半中继续重复上面的方法进行查找。这种每次都从中间元素开始比较,并且一次比较后就能把查找范围缩小一半的方法,叫作二分查找。时间复杂度O(logN)二分査找时间复杂度:log2(n)推导:因为二分查找每次排除掉…半的不适合值,所以对于n个元素的情况:一次二分剩下:n%2F2两次二分剩下:n%2F2%2F2+%3D+n%2F4m次二分剩下:n%2F(2%5Em)在最坏情况下是在排除到只剩下最后一个值之后得到结果,即n%2F(2%5Em)%3Dl所以由上式可得:2%5Em%3Dn进而可求出时间复杂度为:log2(n)注意:log2(l000000)+≈+19.9log2(100000000)+≈+26.6二分查找算法描述用一维数组a存储有序元素序列,用变量l和r分别表示查找范围中第一个元素和最后一个元素的下标,mid表示查找范围的中间位置对应元素的下标,x为要查找的元素。变量初始化,令l%3D1,r%3Dn。l和r分别初始化为有序序列的第一个元素和最后一个元素的下标。判断查找范围l≤r是否成立,如果成立,执行(3),否则输出"-1"(表示没有找到),结束算法。取中间元素,令mid%3D(l%2Br)%2F2,a%5Bmid%5D就是中间元素。比较a%5Bmid%5D与x,如果a%5Bmid%5D等于x,则查找成功,结束算法%3B如果x<a%5Bmid%5D,则在序列的前半部分进行查找,修改查找的上界r%3Dmid-1,下界不变,否则将在序列的后半部分进行在找,修改查找的下界l%3Dmid%2B1,上界不变,转到(2)。特别注意:使用二分查找时,必须保证数据是有序的,若数据是无序的,则需要使用排序算法将数据变得有序%2C即上文中提到的单调性二分查找注意点:二分查找元素X在序列中是否存在二分查找的左边界:第一次出现的位置(序列中可能存在多个目标值,找到第一个>%3DX的元素位置)二分查找的右边界:最后一次出现的位置(序列中可能存在多个目标值,找到最后一个%3D%3DX的元素位置+或者+第一个大于X的元素位置)
二分查找算法的框架如下:
int fun(int a[],int n,int x) //在一个有n个数的有序数组a中查找目标值x
{
int l=1,r=n,mid;
while(l<=r) //判断查找范围l<=r是否成立
{
mid=(l+r)/2; //取中间元素的位置
if(x==a[mid]) //x已经找到
{
return mid; //返回x对应的下标
}
else if(x<a[mid])
{
r=mid-1; //调整r,在前半部分查找
}
else l=mid+1; //调整l,在后半部分查找
}
return -1;
}
例题1:二分查找
请在一个有序递增数组中(不存在相同元素),采用二分查找,找出值 x的位置,如果 x在数组中不存在,请输出 -1 !输入数据:第一行,一个整数 n ,代表数组元素个数(n≤10^6)第二行,n 个数,代表数组的 n 个递增元素(1≤数组元素值≤10^8 )第三行,一个整数 x,代表要查找的数(0≤x≤10^8)输出x 在数组中的位置,或者 -1。
输入数据:
10
1 3 5 7 9 11 13 15 17 19
3
输出数据:
2
解法一:暴力,超时。思路:使用循环暴力的一个一个的查找
#include<bits/stdc++.h>
using namespace std;
int a[1000005];
int main(){
int n,i,f=0,x;
cin>>n;
for(i=1;i<=n;i++)
{ cin>>a[i]; }
cin>>x;
for(i=1;i<=n;i++){
if(a[i]==x){
cout<<i;
f=1; break;
}
}
if(f==0){cout<<-1<<endl;}
}
解法二:二分查找思路:使用二分查找的思路不断的缩小查找范围。
注意:本题的数据量较大,最多达到10^8,对于百万级别及以上的数据量有时候我们使用二分也会出现超时的情况,那是因为使用cin,cout以及换行命令endl的效率比较低,那么我们就要添加ios::syncwithstdio(0);与cin.tie(0);cout.tie(0);并且把endl更换成"\n"进行优化。具体体现在例题2中。原因如下:cin,cout之所以效率低,是因为先把要输出的东西存入缓冲区,再输出,导致效率降低,而使用ios::syncwithstdio(0);与这条语句可以来打消iostream的输入 输出缓存,可以节省许多时间。在标准 C++ 中,cin 和 cout 会同步输出。这意味着,如果你在调用 cin 读取输入之前调用了 cout,那么 cout 的输出会先被缓冲(也就是存储在内存中),直到你调用了 cin 读取输入之后,缓冲中的输出才会被真正输出到屏幕上。cin.tie(0); 的作用就是解除这种同步,使得 cout 的输出不再被缓冲,而是直接输出到屏幕上。这样,你就可以在调用 cin 读取输入之前,就可以先调用 cout 输出内容。endl除了换行外还有清空、刷新缓冲区的作用。对于“刷新缓冲区”的理解:从键盘输入一段字符,不会立即显示在屏幕上,而是先存入缓冲区,再从缓冲区拿出来这样一个过程,而'\n'没有清空、刷新缓冲区的功能即意味着带有'\n'的语句可能比带endl的语句在屏幕上显示出的速度稍慢。带endl的语句会显示的快一点。当然,因为endl比'\n'多一些操作,在效率上endl是慢于'\n'的。
#include<bits/stdc++.h>
using namespace std;
int a[1000005];
int main() {
ios::sync_with_stdio(0);
int k, n;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
cin >> k;
int left = 1, right = n, mid; //left right分别表示序列中第一个和最后一个元素
while (left <= right) {
mid = (left + right) / 2; //找中间值
if (a[mid] == k) {
cout << mid; //找到了 结束程序
return 0;
} else if (k < a[mid]) { //目标值小于中间值 继续查找左半部分
right = mid - 1;
} else if (k > a[mid]) { //目标值大于中间值 继续查找右半部分
left = mid + 1;
}
}
cout << -1; //如果都没有找到则输出-1
return 0;
}
例题2:查找m个数(主题库2646)
请你输入一个含 n个数字的不重复数列,请你高速的在这个数列中寻找 m 个数字 x1,x2,...,x_m ,如果能找到直接输出,如果不存在输出 -1 ,用换行隔开(0<m<n<=10^6)输入格式:输入共 4 行,第一行,为一个数字 n。第二行为n个数字。第三行为一个数字m。第四行为m个要寻找的数字。输出格式:输出共 m 行,每行一个数字,如果能找到直接输出原数字,如果找不到,输出 −1。
样例输入:
5
1 2 3 4 5
3
2 1 3
样例输出:
2
1
3
问题分析:解法一:暴力,样例没问题,提交0分。
#include<bits/stdc++.h>
using namespace std;
int a[1000001];
int main()
{
int n,m,i,j,t;
cin>>n;
for(i=1;i<=n;i++)
{
cin>>a[i];
}
cin>>m;
for(i=1;i<=m;i++)
{
cin>>t;
for(j=1;j<=n;j++)
{
if(a[j]==t)
{
cout<<j<<endl;
break;
}
}
}
return 0;
}
解法2:二分查找,输入超时
#include<bits/stdc++.h>
using namespace std;
const int N = 1000005;
int a[N];
int main() {
int n, m, i, j, t;
cin >> n;
for (i = 1; i <= n; i++) {
cin >> a[i];
}
sort(a + 1, a + 1 + n); //给数组排序,确保单调性前提
cin >> m;
for (i = 1; i <= m; i++) {
cin >> t;
/*
left:第一个元素
right:最后一个元素
mid:中间值
f:标记变量 0表示找不到 1表示找到了
*/
int left = 1, right = n, mid, f = 0;
while (left <= right) {
mid = (left + right) / 2; //找中间值
if (a[mid] == t) {
f = 1; //找到了 修改标记标量 并结束当前查找
break;
} else if (t < a[mid]) { //目标值小于中间值 继续查找左半部分
right = mid - 1;
} else if (t > a[mid]) { //目标值大于中间值 继续查找右半部分
left = mid + 1;
}
}
if (f == 1) { //通过判断标记变量输出结果
cout << t << endl;
} else {
cout << -1 << endl;
}
}
}
解法3:添加优化语句,代码可以AC
#include<bits/stdc++.h>
using namespace std;
const int N = 1000005;
int a[N];
int main() {
ios::sync_with_stdio(0); //优化输入输出缓存
cin.tie(0); //解除cin cout同步
int n, m, i, j, t;
cin >> n;
for (i = 1; i <= n; i++) {
cin >> a[i];
}
sort(a + 1, a + 1 + n);//给数组排序,确保单调性前提
cin >> m;
for (i = 1; i <= m; i++) {
cin >> t;
/*
left:第一个元素
right:最后一个元素
mid:中间值
f:标记变量 0表示找不到 1表示找到了
*/
int left = 1, right = n, mid, f = 0;
while (left <= right) {
mid = (left + right) / 2; //找中间值
if (a[mid] == t) {
f = 1;
break;
} else if (t < a[mid]) { //目标值小于中间值 继续查找左半部分
right = mid - 1;
} else if (t > a[mid]) { //目标值大于中间值 继续查找右半部分
left = mid + 1;
}
}
if (f == 1) { //通过判断标记变量输出结果
cout << t << "\n"; // "\n"取消输出缓冲器刷新
} else {
cout << -1 << "\n";
}
}
}
例题3:找朋友(主题库1189)
小学毕业后,同学们都进入了不同的初中,小明非常想念小伙伴们,所以他打算联系小学的同学们。 现在他得到了市内某所初中的所有名单,找出其中小明的小伙伴们。输入输出格式输入第一行一个整数n,表示某初中人数。接下来n行,每行一个字符串,只有小写字母组成,表示该校每个人的拼音。数据保证没有人拼音相同,且已经按照字典序从小到大排序。第n+2行有一个整数m,表示小明的小伙伴个数。最后m行,每行一个字符串,只有小写字母组成,表示每个小伙伴的拼音,同样保证没有重复。输出输出所有在该校的小伙伴的拼音。每行一个拼音,顺序按照小伙伴给出的顺序。
样例输入:
3
alice
bob
zhangsan
2
lisi
zhangsan
样例输出:
zhangsan
数据范围对于70%的数据,n<=1000,m<=100对于100%的数据,n<=100000,m<=10000,每个人拼音长度不超过15。所有数据,学校学生名单中的姓名,都是按照字典序从小到大排序。问题分析: 本题我们可以使用二分查找+字符串操作实现。学生的名单按照字典序排好是使用二分思想解题的前提条件,然后使用二分方法根据字典序不断缩小查找范围。
#include<bits/stdc++.h>
using namespace std;
int m, n;
string a[100005], t;
//二分查找方法
int fun(string t) {
int left = 1, right = n, mid;
while (left <= right) {
mid = (left + right) / 2;
if (a[mid] == t) { //如果中间就是目标值 返回中间值下标
return mid;
} else if (t > a[mid]) {
left = mid + 1;
} else if (t < a[mid]) {
right = mid - 1;
}
}
return -1;
}
int main() {
ios::sync_with_stdio(0); //优化输入输出缓存
cin.tie(0); //解除cin cout同步
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
cin >> m;
for (int i = 1; i <= m; i++) {
cin >> t;
if (fun(t) != -1) { //如果能找到输出这个人姓名
cout << t << endl;
}
}
return 0;
}
习题训练:和为给定数(主题库2631)
二分查找左边界题目描述请在一个有序不递减的数组中(数组中可能有相等的值),采用二分查找,找到值 x第 1 次出现的位置,如果不存在 x请输出 -1。请注意:本题要求出 q个 x,每个 x在数组中第一次出现的位置。比如有 6 个数,分别是:1,2,2,2,3,3那么如果要求找 3 个数:3,2,5在数组中第一次出现的位置,答案是:5,2,-1输入格式第一行,一个整数 n,代表数组元素个数(n≤10^5)第二行,n个整数,用空格隔开,代表数组的 n 个元素(1≤数组元素的≤10^8 )第三行,一个整数 q,代表有要求出 q个数首次出现的位置(q≤10^5)第四行,q 个整数,用空格隔开,代表要找的数(1≤要找的数≤10^8 )输出格式输出一行,含 q 个整数,按题意输出要找的每个数在数组中首次出现的位置,如果不存在这样的数,请输出 -1。
输入数据#1
6
1 2 2 2 3 3
3
3 2 5
输出数据#1
5 2 -1
二分査找左边界注意点:(1) 二分查找,如果a[mid] == x,还要向左侧看;(2) 找左边界的本质:找数组中第一个>=X的元素的位置;(3) 找到位置L之后,要判断a[L]==x (注意,如果都是负数,找0,要判断L在下标范围内)问题分析:
#include<bits/stdc++.h>
using namespace std;
long long a[100005], n, q, t;
int fun(int x) {
int left = 1, right = n, mid;
while (left <= right) {
mid = (left + right) / 2;
if (a[mid] == x) {
//查找左边界的核心,找到正确答案之后继续往左边查找,看看是否还存在等于X的值
right = mid - 1;
} else if (x < a[mid]) {
right = mid - 1;
} else if (x > a[mid]) {
left = mid + 1;
}
}
/*
如果该值存在于数组中,那么在找到相等值后right边界会不断向左收缩,直到right索引位于X目标值的左边一个索引,而退出循环的条件刚 好是left=right+1,刚好是X的左侧边界索引值!
*/
if (a[left] == x) {
return left;
} else {
return -1;
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
cin >> q;
for (int i = 1; i <= q; i++) {
cin >> t;
cout << fun(t) << " ";
}
}
关于左边界解题的FAQ:
Q:为什么left是正确的返回值?
A:如果该值存在于数组中,那么在找到相等值后right边界会不断向左收缩,直到right索引位于X目标值的左边一个索引,而退出循环的条件刚好是left=right+1,刚好是X的左侧边界索引值!
Q:为什么没有找到时能正确返回-1?
A:没有找到X返回-1时有两种情况 一种是X大于数组中的所有值,如果X大于数组中的所有值,那么right永远不会改变,即right = n,left边界会不断收缩,直到退出循环时left=right+1,即left=n+1;此时left超出边界,不能用a[left] != X进行判断。另一种情况是X没有超出边界,但是不存在,这时只需要判断left索引对应的值是否等于X即可。这里还能判断X小于数组中所有值的情况,因为这时left的值始终不会改变,没有超出边界,可以通过a[left] != X进行判断。二分查找右边界题目描述请在一个有序不递减的数组中(数组中可能有相等的值),采用二分查找,找到值 x最后 1次出现的位置,如果不存在 x请输出 -1。请注意:本题要求出 q个 x,每个 x 在数组中最后一次出现的位置。比如有 6 个数,分别是:1,2,2,2,3,3那么如果要求找 3 个数:3,2,5,在数组中最后一次出现的位置,答案是:6,4,-1输入格式:第一行,一个整数 n,代表数组元素个数(n≤10^5)第二行,n 个整数,用空格隔开,代表数组的 n 个元素(1≤数组元素的值≤10^8 )第三行,一个整数 q,代表有要求出 q 个数最后一次出现的位置(q≤10^5)第四行,q个整数,用空格隔开,代表要找的数(1≤要找的数 ≤10^8)输出格式:输出一行,含 q个整数,按题意输出要找的每个数在数组中最后一次出现的位置,如果不存在这样的数,请输出 -1。
输入数据#1
6
1 2 2 2 3 3
3
3 2 5
输出数据#1
6 4 -1
二分查找右边界注意点:(1) 二分查找,如果a[mid] == x,还要向右侧看,判断右侧是否还是x;(2) 找右边界的本质:找的值L,是数组中第一个>x的元素的位置;(3) 因此要判断L-1 (或者R)的值是否和x相等(a[L-1]==x);程序如下∶
#include<bits/stdc++.h>
using namespace std;
int a[1000010],p;
int n,q;
int f(int x){
int left=1,right=n,mid;
while(left<=right){
mid=(left+right)/2;
if(a[mid]>x){
right=mid-1;
}
else if(x>a[mid]){
left=mid+1;
}
//查找右边界的核心,找到正确答案之后继续往右边查找,看看是否还存在等于X的值
else if(x==a[mid]){
left=mid+1;
}
}
/*
如果该值存在于数组中,那么在找到相等值后left边界会不断向右收缩,直到left索引位于目标值X的右边一个索引,而退出循环的条 件刚好是left=right+1,即right=left-1,right刚好是X的右侧边界索引值!
*/
if(a[right]==x) {
return right;
}
else {
return -1;
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
cin>>q;
for(int i=1;i<=q;i++){
cin>>p;
cout<<f(p)<<" ";
}
return 0;
}
关于右边界解题的FAQ:
Q:为什么right是正确的返回值?
A:如果该值存在于数组中,那么在找到相等值后left边界会不断向右收缩,直到left索引位于目标值的X右边一个索引,而退出循环的条件刚好是left=right+1,即right=left-1,right刚好是X的右侧边界索引值!
Q:为什么没有找到时能正确返回-1?
A:没有找到X返回-1时有两种情况:一种是target小于数组中的所有值,如果X小于数组中的所有值,那么left永远不会改变,即left = 0,right边界会不断收缩,直到退出循环时right=left-1,即right=-1;此时right超出边界,不能用a[right] != X进行判断。另一种情况是X没有超出边界,但是不存在,这时只需要判断right索引对应的值是否等于X即可。这里还能判断X大于数组中所有值的情况,因为这时right的值始终不会改变,没有超出边界,可以通过a[right] != X进行判断。二分函数(参考)binarysearch():二分查找函数binarysearch(a+开始, a+结束+1, x, cmp);函数含义:在a数组的下标区间内,按照cmp的排序规则,找元素x,找到返回true,找不到返回false。注意点:(1)查找区间:结束位置后一个位置,和sort()函数一致;(2)排序规则cmp可以不写默认是升序。如果写的话,查找时的排序规则,必须和排序的规则是一致的;
例子:
#include<bits/stdc++.h>
using namespace std;
int cmp(int x,int y){
return x>y;
}
int main(){
int a[6]={20,10,50,30,60,40};
sort(a+0,a+5+1);
cout << binary_search(a+0,a+5+1,20)<< endl;
cout << binary_search(a+0,a+5+1,36)<<endl;
sort(a,a+6,cmp);
cout << binary_search(a+0,a+5+1,20)<<endl;
cout << binary_search(a+0,a+5+1,20,cmp)<<endl;
return 0;
}
输出结果
1
0
0
1
lowerbound():二分査找左边界
lowerbound(a+开始,a+结束+1,x,cmp);
函数含义:在a数组的下标区间内,按照cmp的排序规则,找元素x的左边界(第一个 >=元素x的位置),返回位置指针;(指针(Pointer): 变量的地址,通过它能找到以它为地址的内存单元。)
例子:
#include<bits/stdc++.h>
using namespace std;
int main(){
int a[6]={20,10,50,20,20,40};
sort(a,a+5+1);//10 20 20 20 40 50
int *p=lower_bound(a+0,a+5+1,20);
// cout << p << " "<< *p << endl;
// cout << p-a<< endl;
cout << lower_bound(a,a+6,20)-a << endl;
cout << lower_bound(a,a+6,15)-a << endl;
cout << lower_bound(a,a+6,60)-a << endl;
return 0;
}