二分查找算法


一、算法思想:

二分查找 也称作折半查找,要求待查找的序列是 有序 的,每次取 中间位置的值 与待查找的数字进行比较。如果中间位置的值 大于 查找数字,则在序列的 前半段 循环查找的过程,如果中间位置的值 小于 查找数字,则在序列的 后半段 循环查找的过程,直到查找到为止,否则序列中没有待查找的数字。

二、算法优势:

为什么要用二分查找?二分查找有怎样的优势呢?让我们通过一个例子来了解一下。
举个栗子:
小明在 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 位置上的值是 66 是大于 3 的,所以待查的数字 3前半段 序列中,将右边界进行更新:

right = mid-1 = 5; //mid上的值已经判断不等于目标数字了,所以右边界可以取 mid 前一位

更新之后的序列如下:

1 2 3 4 5

此时的中间值为:

left = 1;
right = 5;
mid = (left+right)/2 = 3;

接着拿中间位置的值和数字 3 进行比较,下标 3 位置上的值是 33 等于 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 位置上的值是 77 是大于 3 的,所以待查的数字 3前半段 序列中,将右边界进行更新:

right = mid-1 = 5; //mid上的值已经判断不等于目标数字了,所以右边界可以取 mid 前一位

更新之后的序列如下:

1 3 3 4 5

此时的中间值为:

left = 1;
right = 5;
mid = (left+right)/2 = 3;

接着拿中间位置的值和数字 3 进行比较,下标 3 位置上的值是 33 等于 3 ,但是我们发现下标 3 位置的 3 并不是第一个 3 ,下标 2 位置才是第一个。所以针对单调不减序列,我们需要在等于的时候,继续更新右边界:

right = mid-1 = 2; 

更新之后的序列如下:

1 3

此时的中间值为:

left = 1;
right = 2;
mid = (left+right)/2 = 1;

接着拿中间位置的值和数字 3 进行比较,下标 1 位置上的值是 11 小于 3 ,所以待查的数字 3后半段 序列中,将左边界进行更新:

left = mid+1 = 2; // mid上的值已经判断不等于目标数字了,所以左边界可以取 mid 后一位

更新之后的序列如下:

3

此时的中间值为:

left = 2;
right = 2;
mid = (left+right)/2 = 2;

接着拿中间位置的值和数字 3 进行比较,下标 2 位置上的值是 33 等于 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 的个数

以上为个人观点,欢迎指正

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值