折半查找(二分查找)算法+例题理解

一、需满足的两个条件

1.必须是数组(顺序表)

2.必须有序

二、过程演示

如有以下数组(有序数组,并且是从小到大排序):

1.找27 (能找到的情况)

①left,right分别指向数组的两端(用于确定数组的范围)

②用mid=(left+right)/ 2 确定mid的位置,注意(left+right)/ 2 是向下取整!!!

③用mid指向的数与要查找的27做对比,发现36>27,所以查找范围缩小到左边(right移到mid左边)

④重复操作②

 ⑤对比20与27,发现20<27,所以查找范围缩小到右边(left移到mid右边)

⑥重复操作②,mid =(4+5)/ 2 向下取整为4,此时mid指向4位置,发现27=27,所以找到了!

2.找68(找不到的情况) 

①left,right分别指向数组的两端(用于确定数组的范围)

②用mid=(left+right)/ 2 确定mid的位置,注意(left+right)/ 2 是向下取整!!!

③用mid指向的数与要查找的68做对比,发现68>36,所以查找范围缩小到右边(left移到mid右边)

④重复操作②

 ⑤对比60与68,发现60<68,所以查找范围缩小到右边(left移到mid右边)

 ⑥重复操作②,mid =(10+11)/ 2 向下取整为10,此时mid指向10位置,发现67<68,所以left移到mid右侧

⑦重复操作②,mid=(11+11)/ 2=11

⑧对比68和71发现,68<71,所以right移到mid左边

⑨此时left在右边,right却在左边了,即left>right,意味着没有查找范围了,意味着没有查找范围了

三、代码编写(为什么容易写错,怎样避免写错)

为什么容易出错呢?我们可以通过下面的例子来加以理解

可以看到,仅在一些细节上有差异的查找内容,查找出来的结果却是大不相同的! 

查找的答案:

为什么可以这么处理,我们一起来看下面。

1.一个新的角度

从一个新的角度看二分查找,因为在进行二分查找的数组中,是有序的(从小到大或者从大到小),假设我们要找到第一个>=5的元素,我们就需要界定这个数组的红蓝边界,即K-1和K之间,K在这里是未知的。由此可以知道蓝色区域均为<5的元素,红色区域均为>=5的元素

总结:这个问题的最终目标是把蓝红边界找出来,即求出未知数K。

2.朴素算法

做法1:假设一开始整个数组(数组是从小到大排序的)都是没有颜色的,设计一个蓝色指针在数组最左侧,然后不断移动此指针直至其到达蓝红边界。

做法2:假设一开始整个数组(数组是从小到大排序的)都是没有颜色的,设计一个红色指针在数组最右侧,然后不断移动此指针直至其到达蓝红边界。

这样即可求得蓝红边界。

但是此朴素算法的时间复杂度是O(n),算法效率很低。之所以效率低下,是因为其每次只能拓展一个元素。

那么,我们是不是可以用加速拓展的过程来提高算法效率呢?

答案是当然可以!那就需要用到我们的二分查找了!

3.二分查找算法

①数组初始状态(全无颜色)

②找到无颜色区域中间的那个元素,如图所示。

③发现这个中间元素是属于蓝色区域范畴的(属于蓝色区域有条件,比如小于5),于是直接拓展蓝色区域到这个元素的位置!

④继续这样的操作,找到剩下无颜色区域的中间元素

⑤发现这个中间元素是属于红色区域范畴的(属于红色区域也有条件,比如大于等于5),于是直接拓展红色区域到这个元素的位置!

⑥继续上面相同的操作,直至整个数组都有颜色时,就可以找到蓝红边界了!

二分查找的时间复杂度为O(logn),这是因为我们每次循环中都将无颜色区域的长度缩小一半,因此我们大概需要logn次便可以将无颜色区域的长度缩小为0。(logn一般以2为底来计算)

4.二分查找伪代码

假设数组是从下标0开始到N-1结束(即数组有N个元素)

细节一:为什么不能初始化l=0,r=N-1

此时如果问题是找到小于等于4的第一个数,那么此时整个数组均为红色区域(>4),此时会产生bug,因为蓝色区域实际上在数组外边。

细节二:m始终处于[0,N)以内,即m不会溢出

细节三:更新指针时,能不能写成l=m+1,或者r=m-1

这样会容易出错!

细节四:while l+1不等于r,不会陷入死循环

例题:数的范围

题目概述:

给定一个按照升序排列的长度为n的整数数组,以及q个查询。

对于每个查询,返回一个元素k的起始位置和终止位置(位置从0开始计数)

如果数组中不存在该元素,则返回-1 -1。

输入格式:

第一行包含整数n和q,表示数组长度和询问个数。

第二行包含 n个整数(均在1~ 10000 范围内),表示完整数组。

接下来q行,每行包含一个整数k,表示一个询问元素。

输出格式:

共q行,每行包含两个整数,表示所求元素的起始位置和终止位置,如果数组中不存在该元素,则返回-1 -1。

数据范围:

1 ≤n≤ 100000
1 ≤q≤ 10000
1 ≤k≤ 10000

代码编写:

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

const int N = 100010; 

int n,q;
int arr[N];

//数组中的数是num,查找的数是x
bool isBlue1(int num,int x){
	if(num<x)return true;
	else return false;
}

//第一个二分查找找的是:被询问数的第一次出现的位置(下标) 
int binary_search1(int arr[],int len,int x){
	int l=-1,r=len;
	while(l+1<r){  //l+1!=r是l+1<r的子集 
		int mid=(l+r)/2;
		if(isBlue1(arr[mid],x)){
			l=mid;
		}
		else r=mid;
	}
	if(arr[r]==x)return r;
	else return -1;  //找不到就返回-1 
}

//数组中的数是num,查找的数是x 
bool isBlue2(int num,int x){
	if(num<=x)return true;
	else return false;
}

//第二个二分查找找的是:被询问数的次出现的位置(下标)
int binary_search2(int arr[],int len,int x){
	int l=-1,r=len;
	while(l+1<r){
		int mid=(l+r)/2;
		if(isBlue2(arr[mid],x)){
			l=mid;
		}
		else r=mid;
	}
	if(arr[l]==x)return l;
	else return -1;  //找不到就返回-1 
}

int main(){
	scanf("%d %d",&n,&q);
	for(int i=0;i<n;i++){
		scanf("%d",&arr[i]);
	}
	while(q--){ 
		int x;
		scanf("%d",&x);
		int res1=binary_search1(arr,n,x);//第一次查找x的起始位置 
		int res2=binary_search2(arr,n,x);//第二次查找x的终止位置
		printf("%d %d",res1,res2);
	}
	return 0;
}

运行样例:

洛谷:P2249 【深基13.例1】查找

题目描述

输入 n 个不超过 109 的单调不减的(就是后面的数字不小于前面的数字)非负整数 a1​,a2​,…,an​,然后进行 m 次询问。对于每次询问,给出一个整数 q,要求输出这个数字在序列中第一次出现的编号,如果没有找到的话输出 −1 。

输入格式

第一行 2 个整数 n 和 m,表示数字个数和询问次数。

第二行 n 个整数,表示这些待查询的数字。

第三行 m 个整数,表示询问这些数字的编号,从 1 开始编号。

输出格式

输出一行,m 个整数,以空格隔开,表示答案。

输入输出样例

输入

11 3
1 3 3 3 5 7 9 11 13 15 15
1 3 6

输出

1 2 -1 

说明/提示

数据保证,1≤n≤106,0≤ai​,q≤109,1≤m≤105

本题输入输出量较大,请使用较快的 IO 方式。

解题代码

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1000010;

bool isBlue(int num,int x){
	if(num<x){
		return true;
	}
	else return false;
}

int binary_search(int q[],int len,int x){
	int l=0,r=len+1;
	while(l+1<r){
		int m=(l+r)/2;
		if(isBlue(q[m],x)){
			l=m;
		}
		else r=m;
	}
	if(q[r]==x){
		return r;
	}
	else return -1;
}

int n,m;
int q[N];

int main(){
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&q[i]);
	}
	while(m--){
		int x;
		scanf("%d",&x);
		int res=binary_search(q,n,x);
		printf("%d ",res);
	}
	return 0;
}  

关键思路

用二分查找的思路

洛谷:P1102 A-B 数对

题目背景

出题是一件痛苦的事情!

相同的题目看多了也会有审美疲劳,于是我舍弃了大家所熟悉的 A+B Problem,改用 A-B 了哈哈!

题目描述

给出一串正整数数列以及一个正整数 C,要求计算出所有满足 A−B=C 的数对的个数(不同位置的数字一样的数对算不同的数对)。

输入格式

输入共两行。

第一行,两个正整数 N,C。

第二行,N 个正整数,作为要求处理的那串数。

输出格式

一行,表示该串正整数中包含的满足 A−B=C 的数对的个数。

输入输出样例

输入 

4 1
1 1 2 3

输出 

3

说明/提示

对于 75% 的数据,1≤N≤2000。

对于 100% 的数据,1≤N≤2×105,0≤ai​<230,1≤C<230。

2017/4/29 新添数据两组

解题代码:

当我尝试用常规的方法进行解题时,写出以下代码,进入测试后发现还有一些示例不能通过:

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N=200010;

int n,C;
int q[N]; 

int main(){
	scanf("%d %d",&n,&C);
	for(int i=1;i<=n;i++){
		scanf("%d",&q[i]);
	}
	
	int cnt=0;//计数
	for(int a=1;a<=n;a++){
		for(int b=1;b<=n;b++){
			if(q[a]-q[b]==C){
				cnt++;
			}
		}
	} 
	printf("%d\n",cnt);
	return 0;
} 

这是为什么呢?我们开始进行分析!

通过上图我们可以知道是“时间超限”的问题:

c++一秒算10的9次方比较吃力,基本10的9次方就跑不出来了,那么最好是把运算数量控制到10的7次方~10的8次方。

分析n:

由题目知道,对于百分之百的数据n的取值最大为2*10^5,而且n经历了嵌套,所以运算数量会有4*10^10那么多,超出了10的9次方!

分析cnt:

int的最大值的2147483647,将它估计为2*10^9,再分析计数cnt最大可能有10^5*10^5那么大,即10^10,因为2*10^9小于10^10,所以用int来定义cnt可能不够,所以改成long long

int的最大值有2^31-1,long long的最大值有2^63-1,约为1*10^19

具体怎么解决呢,肯定要对计算的式子进行优化吧!

换一个思路,我们使用B+C=A进行查找计数,那么我们需要对B进行枚举,在取到B的每一个可能的取值时对其加上C,再查找A,这种情况下A有几个就进行几次cnt++。那么我们开始对这个想法进行算法优化,优化如下:

for(int B=1;B<=n;B++){

        int A=B+C

        二分查找,找到A。(需要找log以2为底2*10^5,计算得需要大约17次)

}

所以此时查找需要查找2*10^5*17次

下面是引入二分查找的代码(通过率为100%):

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N=200010;

int n,C;
int q[N]; 

int bSearch1(int q[],int len,int x){
	int l=0,r=n+1;
	while(l+1<r){
		int mid=(l+r)/2;
		if(q[mid]<x){
			l=mid;
		}
		else r=mid;
	}
	if(q[r]==x) return r;
	else return -1;
}

int bSearch2(int q[],int len,int x){
	int l=0,r=n+1;
	while(l+1<r){
		int mid=(l+r)/2;
		if(q[mid]<=x){
			l=mid;
		}
		else r=mid;
	}
	if(q[l]==x) return l;
	else return -1;
}

int main(){
	scanf("%d %d",&n,&C);
	for(int i=1;i<=n;i++){
		scanf("%d",&q[i]);
	}
	
	//排序:因为数据从数组下标为1处开始存,若是从0开始就是sort(q,q+n) 
	sort(q+1,q+1+n);
	
	long long cnt=0;//计数
	for(int B=1;B<=n;B++){
		int A=q[B]+C;
		int res1=bSearch1(q,n,A);
		if(res1==-1){
			continue;
		}
		else{
			int res2=bSearch2(q,n,A);
			cnt += res2-res1+1;
		}
	} 
	printf("%lld\n",cnt);//cnt是long long型,注意输出! 
	return 0;
} 

此文章中的图片部分截自网络,供小编和大家共同参考学习。如有错误请指正,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值