《算法导论》学习笔记

第4章入门篇(2)——算法初步(5)

4.5 二 分

4.5.1 二分查找

一个经典的问题:如何在一个严格递增序列A中找出给定的数x
最直接的办法是:线性扫描序列中的所有元素,如果当前元素恰好为x,则表明查找成功;如果扫描完整个序列都没有发现给定的数x,则表明查找失败,说明序列中不存在数x。这种顺序查找的时间复杂度为O(N)。更好的办法便是使用二分查找:二分查找是基于有序序列的查找算法,该算法一开始令[left,right]为整个序列的下标区间,然后每次测试当前[left,right]的中间位置mid=(left+right)/2,判断A[mid]与与查询的元素x的大小:①如果A[mid]==x,说明查找成功,退出查询。②如果A[mid]>x,说明元素x在mid位置的左边,因此往左区间[left,mid-1]继续查找。③如果A[mid]<x,说明元素x在mid位置的右边,因此往右区间[mid+1,right]继续查找。时间复杂度是0(logn)。
注:二分查找的过程与序列的下标从0开始还是从1开始无关

#include<iostream>
using namespace std;
int binarySearch(int A[],int left,int right,int x){
	int mid;
	while(left<=right){
		mid=(left+right)/2;
		if(A[mid]==x) return mid;
		else if(A[mid]>x) right=mid-1;
		else left=mid+1;
	}
	return -1;
}
int main(){
	const int n=10; 
	int A[n]={1,3,4,6,7,8,10,11,12,15};
	printf("%d %d\n",binarySearch(A,0,n-1,6),binarySearch(A,0,n-1,9));
	return 0;
}

注: 如果二分上届超过int型数据范围的一半,那么当欲查询元素在序列较靠后的位置时,语句mid=(left+right)/2中的left+right就有可能超过int而导致溢出,此时一般使用mid=left+(right-left)/2这条等价语句作为代替以避免溢出。

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

(1)求序列中的第一个大于等于x的元素位置
①如果A[mid]>=x,说明第一个大于等于x的元素的位置一定在mid处或mid的左侧,应往左子区间[left,mid]继续查询,即令right=mid
②如果A[mid]<=x,说明第一个大于等于x的元素的位置一定在mid处或mid的右侧,应往右子区间[mid+1,right]继续查询,即令leftt=mid+1

int lower_bound(int A[],int left,int right,int x){
	int mid;
	while(left<right){
		mid=(left+right)/2;
		if(A[mid]>=x){
			right=mid;
		}else{
			left=mid+1;
		}
	}
	return left;
}

(2)求序列中的第一个大于x的元素位置
①如果A[mid]>x,说明第一个大于x的元素的位置一定在mid处或mid的左侧,应往左子区间[left,mid]继续查询
②如果A[mid]<=x,说明第一个大于x的元素的位置一定在mid处或mid的右侧,应往右子区间[mid+1,right]继续查询

int upper_bound(int A[],int left,int right,int x){
	int mid;
	while(left<right){
		mid=(left+right)/2;
		if(A[mid]>x){
			right=mid;
		}else{
			left=mid+1;
		}
	}
	return left;
}

lower_bound函数和upper_bound函数都在解决一个问题:寻找有序序列中第一个满足某条件的元素的位置。

寻找有序序列第一个满足某条件的元素的位置的固定模板
(1)二分区间为左闭右闭的,初值必须能覆盖解的所有可能取值

int sovle(int left,int right){
	int mid;
	while(left<right){
		mid=(left+right)/2;
		if(条件成立){
			right=mid;
		}else{
			left=mid+1;
		}
	}
	return left;
}

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

(2)二分区间为左开右闭的

int sovle(int left,int right){
	int mid;
	while(left+1<right){
		mid=(left+right)/2;
		if(条件成立){
			right=mid;
		}else{
			left=mid+1;
		}
	}
	return right;
}

4.5.2 二分法拓展

  • 计算√2
const double eps=le-5;   //精度为10^-5
double f(double x){
	return x*x;
}
double calSqet(){
	double left=1,right=2,mid;
	while(right-left>eps){
		mid=(left+right)/2;
		if(f(mid)>2){
			right=mid;
		}else{
			left=mid;
		}
	}
	return mid;
}

计算√2的近似值的问题是这样一个问题的特例:给定一个定义在[L,R]上的单调函数f(x),求方程f(x)=0的根。

const double eps=le-5;   //精度为10^-5
double f(double x){   //计算f(x)
	return ……;
}
double calSqet(double L,double R){
	double left=L,right=R,mid;
	while(right-left>eps){
		mid=(left+right)/2;
		if(f(mid)>2){
			right=mid;
		}else{
			left=mid;
		}
	}
	return mid;
}
  • 装水问题
#include <iostream>
#include <cmatch>
using namespace std;
const double PI=acos(-1.0);
const double eps=le-5;
double f(double R,double h){
	double alpha=2*acos((R-h)/R);
	double L=2*sqrt(R*R-(R-h)*(R-h));
	double S1=alpha*R*R/2-L*(R-h)/2;
	double S2=PI*R*R/2;
	return S1/S2;
}
double solve(double R,double r){
	double left=0,right=R,mid;
	while(right-left>eps){
		mid=(left+right)/2;
		if(f(R,mid)>r){
			right=mid;
		}else{
			left=mid;
		}
	}
	return mid;
}
int main(){
	double R,r;
	scanf(%lf%lf",&R,&r);
	printf("%.4f\n",solve(R,r));
	return 0;
}
  • 木棒切割问题
#include<iostream>
using namespace std;
int solve(int a[],int left,int right,int k){
	int mid,i,num;
	while(left<right){
		mid=(left+right)/2;
		num=0;
		printf("left=%d,right=%d,mid=%d\n",left,right,mid);
		for(i=0; i<3; i++){
			num=num+a[i]/mid;
		}
		printf("num=%d\n",num);
		if(num<k){
			right=mid;
		}else{
			left=mid+1;
		}
	}
	return left;
}
int main(){
	int a[3]= {10,15,24};
	int n,k,len;
	scanf("%d",&k);
	len=solve(a,0,a[2],k);
	printf("长度为%d",len-1);
}

4.5.3 快速幂
接下来研究一下快速幂的迭代写法。
对ab来说,如果把b写成二进制,那么b就可以写成若千二次幂之和。例如13的二进制是1101,于是3号位、2号位、0号位就都是1,那么就可以得到13=23 +22+20=8+4+1,所以al3=a8+4+1=a8*a4*a1.
通过上面的推导,我们发现al3可以表示成a8、a4、a1的乘积。很容易想象,通过同样的推导,我们可以把任意的ab表示成an、… a8、a4、a2、a1中若干项的乘积,其中如果b的进制的i号位为1那么项a2i就被选中.于是可以得到计算ab的大致思路:令i从0到k枚类b的二进制的每一位,如果当前位为1,那么累积a2i.注意到序列an、… a8、a4、a2、a1的前一项总是等于后一项的平方,因此具体实现的时候可以这么做:
①初始令ans等于1,用来存放累积的结果。
②判断b的二进制末尾是否为1 (即判断b& 1是否为1,也可以理解为判断b是否为奇数),如果是的话,令ans乘上a的值。
③令a平方,并将b右移一位(也可以理解为将b除以2)。
④只要b大于0,就返回②。

typedef long long LL;
LL binaryPow(LL a,LL b,LL m){
	LL ans=1;
	while(b>0){
		if(b&1){
			ans=ans*a%m;
		}
		a=a*a%m;
		b>>=1;
	}
	return ans;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值