一、算法思想:
二分查找 也称作折半查找,要求待查找的序列是 有序 的,每次取 中间位置的值 与待查找的数字进行比较。如果中间位置的值 大于 查找数字,则在序列的 前半段 循环查找的过程,如果中间位置的值 小于 查找数字,则在序列的 后半段 循环查找的过程,直到查找到为止,否则序列中没有待查找的数字。
二、算法优势:
为什么要用二分查找?二分查找有怎样的优势呢?让我们通过一个例子来了解一下。
举个栗子:
小明在 1~1000 中随机选了一个数字,并让小红猜,问有哪些方法,最多要猜多少次?
假如使用枚举法进行逐一枚举,则最多要猜 1000 次,而使用二分法进行查找,最多只需猜
log
2
1000
\log_2{1000}
log21000 = 10 次。
优势:
所以使用二分法对有序序列进行查找时,能够有效的提高程序的执行效率,降低时间复杂度。
三、代码实现:
有序序列(这里只讨论升序的情况,降序也是一个道理)又可以分为单调递增序列和单调不减序列,两者的区别如下:
1.单调递增序列
单调递增序列指的是后面数字一定会大于前面数字的序列,比如:
1 2 3 4 5 6 7 8 9 10 11
假设我们要在这个序列中找到数字 3 ,那么使用二分查找就可以这样实现。
首先我们需要确定 left(左边界) right(右边界) 以及 mid(中间值),则可以得到:
left = 1;
right = 11;
mid = (left+right)/2 = 6;
接着拿中间位置的值和数字 3 进行比较,下标 6 位置上的值是 6 ,6 是大于 3 的,所以待查的数字 3 在 前半段 序列中,将右边界进行更新:
right = mid-1 = 5; //mid上的值已经判断不等于目标数字了,所以右边界可以取 mid 前一位
更新之后的序列如下:
1 2 3 4 5
此时的中间值为:
left = 1;
right = 5;
mid = (left+right)/2 = 3;
接着拿中间位置的值和数字 3 进行比较,下标 3 位置上的值是 3 ,3 等于 3 ,说明找到了该数字,返回对应的下标即可。
例题:
【题目描述】:
在一个互不相同的升序数组中,查找 x 所在的下标。
【输入格式】:
第一行两个整数 n 和 m 。
第二行 n 个数,表示有序的数列。
接下来 m 行,每行一个整数 x ,表示一个询问的数。
【输出格式】:
对于每个询问如果 x 在数列中,输出下标。否则输出 -1
【输入样例】:
5 3
3 4 5 7 9
7
3
8
【输出样例】:
4
1
-1
【提示/说明】:
0<n,m,x<=10^5
C++ 代码实现:
#include<bits/stdc++.h>
using namespace std;
int n,m,target;
int a[100005];
int Binary_search(int t){
int left=1;
int right=n;
while(left<=right){
int mid=(left+right)/2;
if(a[mid]<t) left=mid+1;
else if(a[mid]>t) right=mid-1;
else if(a[mid]==t) return mid;
}
return -1;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=m;i++){
cin>>target;
cout<<Binary_search(target)<<endl;
}
return 0;
}
2.单调不减序列
单调不减序列指的是后面数字不小于前面数字的序列,比如:
1 3 3 4 5 7 9 11 13 15 15
假设我们要在序列中找到数字 3 第一次出现的位置,那么使用二分查找就可以这样实现。
首先我们需要确定 left(左边界) right(右边界) 以及 mid(中间值),则可以得到:
left = 1;
right = 11;
mid = (left+right)/2 = 6;
接着拿中间位置的值和数字 3 进行比较,下标 6 位置上的值是 7 ,7 是大于 3 的,所以待查的数字 3 在 前半段 序列中,将右边界进行更新:
right = mid-1 = 5; //mid上的值已经判断不等于目标数字了,所以右边界可以取 mid 前一位
更新之后的序列如下:
1 3 3 4 5
此时的中间值为:
left = 1;
right = 5;
mid = (left+right)/2 = 3;
接着拿中间位置的值和数字 3 进行比较,下标 3 位置上的值是 3 ,3 等于 3 ,但是我们发现下标 3 位置的 3 并不是第一个 3 ,下标 2 位置才是第一个。所以针对单调不减序列,我们需要在等于的时候,继续更新右边界:
right = mid-1 = 2;
更新之后的序列如下:
1 3
此时的中间值为:
left = 1;
right = 2;
mid = (left+right)/2 = 1;
接着拿中间位置的值和数字 3 进行比较,下标 1 位置上的值是 1 ,1 小于 3 ,所以待查的数字 3 在 后半段 序列中,将左边界进行更新:
left = mid+1 = 2; // mid上的值已经判断不等于目标数字了,所以左边界可以取 mid 后一位
更新之后的序列如下:
3
此时的中间值为:
left = 2;
right = 2;
mid = (left+right)/2 = 2;
接着拿中间位置的值和数字 3 进行比较,下标 2 位置上的值是 3 ,3 等于 3 ,等于的时候继续更新右边界:
right = mid-1 = 1;
更新之后的序列不存在, left 已经大于 right 了。那么下标 2 就为数字 3 第一次出现的位置。
例题:
【题目描述】:
在一个单调不减序列(就是后面的数字不小于前面的数字)中,查找 x 所在的下标。
【输入格式】:
第一行两个整数 n 和 m 。
第二行 n 个数,表示有序的数列。
接下来 m 行,每行一个整数 x ,表示一个询问的数。
【输出格式】:
对于每个询问如果 x 在数列中,输出下标。否则输出 -1
【输入样例】:
11 3
1 3 3 3 5 7 9 11 13 15 15
1 3 6
【输出样例】:
1 2 -1
【提示/说明】:
0<n,m,x<=10^5
C++ 代码实现:
#include<bits/stdc++.h>
using namespace std;
int n,m,target;
int a[100005];
int Binary_search(int t){
int left=1;
int right=n;
int ans=-1; //表示数的位置,默认为-1
while(left<=right){
int mid=(left+right)/2;
if(a[mid]<t) left=mid+1;
else if(a[mid]>=t){
right=mid-1;
ans=mid; //更新答案
}
}
if(a[ans]==t) return ans; //找到了目标数,返回答案
else return -1; //找不到返回-1
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=m;i++){
cin>>target;
cout<<Binary_search(target)<<" ";
}
return 0;
}
注:使用这个代码,同样可以用于求解单调递增序列
四、二分查找函数
1.lower_bound 函数
上述代码的功能可以通过 lower_bound 函数来实现,它的作用是查找 递增 数组内 大于等于 目标值的 第一个元素 的 地址 。
lower_bound (num, num+n, x) //参数分别为起始地址、结束地址、目标数值
使用时需要导入头文件 algorithm :
#include <algorithm>
lower_bound 函数返回的是一个地址,想要求得目标值的下标,可以通过这种方式来实现:
int v = lower_bound (num, num+n, x) - num; //返回地址-数组起始地址
如果 x 大于数组中最大的元素,那么 v 等于数组的长度,如果 x 小于数组中最小的元素,那么 v 等于 0 (若 v=0,x 也有可能等于 num[0] )
则上述代码可以更改为:
#include<bits/stdc++.h>
using namespace std;
int n,m,target;
int a[1000005];
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
}
for(int i=1;i<=m;i++){
cin>>target;
int ans=lower_bound(a+1,a+1+n,target)-a; //数组从1开始存的,所以+1
if(a[ans]==target) cout<<ans<<" ";
else cout<<-1;
}
return 0;
}
2.upper_bound 函数
upper_bound 功能与 lower_bound 极其相似,唯一的区别是,它的作用是查找递增数组内 大于 目标值的第一个元素的地址,而 lower_bound 是 大于等于 。
upper_bound (num, num+n, x) //参数分别为起始地址、结束地址、目标数值
使用时同样需要导入头文件 algorithm :
#include <algorithm>
与 lower_bound 一样,upper_bound 也可以 返回地址-数组起始地址 的方式获取到下标:
int v = upper_bound (num, num+n, x) - num;
upper_bound 可以用来查找比 x 大的元素个数,即:
int v = upper_bound (num, num+n, x) - num; //取得第一个大于 x 的下标
int sum = n - v; // 通过序列最大的下标n -下标v 的形式得到个数,如果下标从1开始,则sum=n-v+1
也可以联合 lower_bound 用于求 x 的个数,即:
int v1 = upper_bound (num, num+n, x) - num; //取得第一个大于 x 的下标
int v2 = lower_bound (num, num+n, x) - num; //取得第一个大于等于 x 的下标
int num = v1 - v2; // 取得 x 的个数
以上为个人观点,欢迎指正