二分法基础

【二分查找】

问题:如何在一个严格递增序列 A [   ] A[\ ] 中找出给定的数 x x

//A[]为严格递增序列,left为二分下界,right为二分上界,x为欲查询的数 

//二分区间为左闭右闭的[left,right],传入的初值为[0,n-1]

int binarySearch(int A[], int left, int right, int x)
{
	int mid;	//mid为left和right的中点
	while(left <= right)	//如果left>right就没办法形成闭区间了
	{
		mid = (left + right) / 2; 		//取中点
		if(A[mid]==x)
			return mid;  		//找到x,返回下标
		else if(A[mid] > x)	//中间的数大于x 
			right = mid - 1;  	//往左子区间[left,mid-1]查找
		else				//中间的数小于x
			left = mid + 1; 	//往右子区间[mid+1,right]查找
	}
	return -1; //查找失败,返回-1
}

注意中间值mid

中间值写成 m i d = l e f t + ( r i g h t l e f t ) / 2 mid = left+(right-left)/2 或者 m i d = ( l e f t + r i g h t ) > > 1 mid = (left+right)>>1 都行。不过, 如果 l e f t + r i g h t left+right 很大, 可能溢出, 用前一种更好。不能写成 m i d = ( l e f t + r i g h t ) / 2 mid = (left+right)/2 ,在有负数的情况下,会出错。

【求序列中第一个大于等于x的元素的位置】

问题:如果递增序列 A [   ] A[\ ] 中的元素可能重复,那么如何对给定的欲查询元素 x x ,求出序列中第一个大于等于 x x 的元素的位置 L L 以及第一个大于 x x 的元素的位置 R R ,这样元素 x x 在序列中的存在区间就是左闭右开区间 [ L , R ) [L, R)

//A[]为递增序列,x为欲查询的数,函数返回第一个大于等于x的元素的位置 

//二分上下界为左闭右闭的[left, right],传入的初值为[0,n]

int lower_bound(int A[], int left, int right, int x)
{
	int mid;	//mid为left和right的中点
	while(left<right)	//对[left,right]来说,left==right意味着找到唯一位置 
	{
		mid = left+(right-left)/2;	//取中点 
		if(A[mid] >= x)		//中间的数大于等于x 
			right = mid;		//往左子区间[left,mid]查找 
		else				//中间的数小于x 
			left = mid + 1;		//往右子区间[mid+1,right]查找 
	}
	return left;	//返回夹出来的位置 
}

注意:

  1. 循环条件为 l e f t < r i g h t left<right , 而非之前的 l e f t r i g h t left ≤ right ,这是由问题本身决定的。
  2. 由于当 l e f t = = r i g h t left==right 时while循环停止,因此最后的返回值既可以是 l e f t left ,也可以是 r i g h t right
  3. 二分的初始区间应当能覆盖到所有可能返回的结果,故二分的初始区间为 [ l e f t , r i g h t ] = [ 0 , n ] [left, right] = [0, n]

【求序列中第一个大于x的元素的位置】

//A[]为递增序列,x为欲查询的数,函数返回第一个大于x的元素的位置
 
//二分上下界为左闭右闭的[left,right],传入的初值为[0,n]

int upper_bound(int A[], int left, int right, int x)
{
	int mid;	//mid为left和right的中点
	while(left < right)	//对[left,right]来说,left==right意味着找到唯一位置 
	{
		mid = left+(right-left)/2;	//取中点 
		if(A[mid] > x)		//中间的数大于x 
			right = mid;		//往左子区间[left,mid]查找 
		else	   			//中间的数小于x 
			left = mid + 1;		//往右子区间[mid+1,right]查找 
	}
	return left;	//返回夹出来的位置 
}

l o w e r _ b o u n d lower\_bound 函数的代码相比, u p p e r _ b o u n d upper\_bound 函数只是把代码中的 A [ m i d ] x A[mid] ≥ x 改成了 A [ m i d ] > x A[mid] > x ,其他完全相同。

【模板】

解决 “寻找有序序列第一个满足某条件的元素的位置” 问题的固定模板。

1、二分区间为左闭右闭的 [ l e f t , r i g h t ] [left, right] ,初值必须能覆盖解的所有可能取值。

所谓的“某条件”在序列中一定是从左到右先不满足,然后满足的(否则把该条件取反即可)。

int solve(int left, int right)
{
	int mid;	//mid为left和right的中点
	while(left<right)	//对[left,right]来说,left==right意味着找到唯一位置 
	{
		mid = left+(right-left)/2;	//取中点 
		if(条件成立)		//条件成立,第一个满足条件的元素的位置<=mid 
			right = mid;		//往左子区间[left,mid]查找 
		else	   			//条件不成立,则第一个满足该条件的元素的位置>mid 
			left = mid + 1;		//往右子区间[mid+1,right]查找 
	}
	return left;	//返回夹出来的位置 
}

如果想要寻找最后一个满足“条件C”的元素的位置,则可以先求第一个满足“条件!C”的元素的位置,然后将该位置减 1 1 即可。

2、使用左开右闭的写法也可以,并且与左闭右闭的写法等价。

//二分区间为左开右闭的(left,right]
//初值必须能覆盖解的所有可能取值, 并且left比最小取值小1
//例如对下标从0开始的序列来说,left和right的取值应为-1和n

int solve(int left, int right)
{
	int mid;	//mid为left和right的中点
	while(left + 1 < right)	//对(left,right]来说,left+1==right意味着找到唯一位置 
	{
		mid = left+(right-left)/2;	//取中点 
		if(条件成立)		//条件成立,第一个满足条件的元素的位置<=mid 
			right = mid;		//往左子区间[left,mid]查找 
		else	   //条件不成立,则第一个满足该条件的元素的位置>mid 
			left = mid;		//往右子区间[mid,right]查找 
	}		//改为left = mid,并且返回的应当是right而不是left
	return right;
}

【STL中的lower_bound()和upper_bound()】

前面扯了那么多,总结起来就是,如果只是简单地找 x x x x 附近的数, 就用 STL 的 l o w e r _ b o u n d ( ) lower\_bound() u p p e r _ b o u n d ( ) upper\_bound() 函数。

l o w e r _ b o u n d ( ) lower\_bound() u p p e r _ b o u n d ( ) upper\_bound() 都是利用二分查找的方法在一个排好序的数组中进行查找的。

在从小到大的排序数组中,

  1. l o w e r _ b o u n d ( b e g i n ,   e n d ,   n u m ) lower\_bound(begin,\ end,\ num) :从数组的 b e g i n begin 位置到 e n d 1 end-1 位置二分查找第一个大于或等于 n u m num 的数字,找到返回该数字的地址,不存在则返回 e n d end 。通过返回的地址减去起始地址 b e g i n begin ,得到找到数字在数组中的下标。

  2. u p p e r _ b o u n d ( b e g i n ,   e n d ,   n u m ) upper\_bound(begin,\ end,\ num) :从数组的 b e g i n begin 位置到 e n d 1 end-1 位置二分查找第一个大于 n u m num 的数字,找到返回该数字的地址,不存在则返回 e n d end 。通过返回的地址减去起始地址 b e g i n begin ,得到找到数字在数组中的下标。

在从大到小的排序数组中,重载 l o w e r _ b o u n d ( ) lower\_bound() u p p e r _ b o u n d ( ) upper\_bound()

  1. l o w e r _ b o u n d ( b e g i n ,   e n d ,   n u m ,   g r e a t e r ( ) ) lower\_bound(begin,\ end,\ num,\ greater() ) :从数组的 b e g i n begin 位置到 e n d 1 end-1 位置二分查找第一个小于或等于 n u m num 的数字,找到返回该数字的地址,不存在则返回 e n d end 。通过返回的地址减去起始地址 b e g i n begin ,得到找到数字在数组中的下标。

  2. u p p e r _ b o u n d ( b e g i n ,   e n d ,   n u m ,   g r e a t e r ( ) ) upper\_bound(begin,\ end,\ num,\ greater() ) :从数组的 b e g i n begin 位置到 e n d 1 end-1 位置二分查找第一个小于 n u m num 的数字,找到返回该数字的地址,不存在则返回 e n d end 。通过返回的地址减去起始地址 b e g i n begin ,得到找到数字在数组中的下标。

发布了690 篇原创文章 · 获赞 103 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/qq_42815188/article/details/104065189
今日推荐