1.二分查找
经典问题:如何再一个严格递增的序列A中找出给定的数x
二分查找:二分查找是基于有序序列的查找算法,该算法一开始令[left,right]为整个下标区间,然后每次测试当前中间位置mid=(left+right)/2,判断A[mid]与欲查询元素x的大小。
1)如果A[mid]==x 说明查找成功,退出查询
2)如果A[mid]>x,说明x再mid位置的左边,因此往左子区间[left,mid-1]查找
3)如果A[mid]<x,说明x再mid位置的右边,因此往右子区间[mid+1,right]查找
时间复杂度O(logn)
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类型的一半,那么当欲查询元素再序列较靠后的位置时,语句mid = (left + right) / 2;
可能会导致溢出,此时可以使用mid = left + (right - left) / 2
这条等价语句避免溢出。
问题二:如果序列A中的元素可能重复,那么如何对给定的欲查询元素x,求出序列中第一个大于等于x的元素的位置L以及第一个大于x的元素位置R,这样元素x再序列中的存在区间就是[L,R)。
如对下标从0开始,有5个元素的序列{1,3,3,3,6}来说,查询3,则L=1,R=4,查询6,则L=4,R=5。
查询8 则L=R=5。
如果序列中没有x,那么L与R可以理解为假设序列中存在x,那么x应当存在的位置。
1)先考虑第一个大于等于x的元素,做法与之前类似,假设当前的二分区间为[left,right],那么可以根据mid位置处的元素与欲查询元素x的大小来判断应该往哪个子区间继续查找。
如果A[mid]>=x,说明第一个大于等于x的元素的位置一定在mid处或mid的左侧,向左区间[left,mid]继续查找,令right=mid
2)如果A[mid]<x,说明第一个大于等于x的元素一定在mid的右侧,应该往右区间[mid+1,right]继续查找,即令left=mid+1。
代码:
int lower_bound(int A[], int left, int right, int x) {
int mid;
while (left < right) {//left==right意味找见唯一位置
mid = (left + right) / 2;
if (A[mid] >= x)right = mid;
else left = mid + 1;
}
return left;//执行到最后,x如果存在left一定是第一个x
}
这里的循环条件为left<right,并非left<=right,是因为在上题中,元素不存在返回-1,这样当left>right时[left,right]就不是闭区间,以此作为元素不存在的判定原则,因此left<=right一直执行,如果想要返回第一个大于等于x元素的位置,就不需要判断x的本身是否存在,因为就算不存在,返回的也是如果存在,它应该在的位置,当left=right时,刚好能够找出唯一的位置,就是需要的结果,返回值既可以返回right,也可返回right。
3)再找出序列中第一个大于x的元素的位置,做法类似。
int upper_bond(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()x`都在解决,寻找有序序列中第一个满足某条件的元素位置。lower_bound找的时第一个 值大于等于x的元素的位置,upper_bond找的时第一个 值大于x的元素的位置
解决此类问题的模板:
比如如果要求最后一个满足条件C的元素,可以求第一个不满足条件C的元素,然后令下标减一。
int solve(int left,int right){左闭右闭区间
int mid;
while(left < right){//left==right说明找到唯一的位置
mid = (left + right) / 2;//取中点
if(条件成立){//条件成立,第一个满足条件的元素的位置<=mid
right = mid;
}
else left = mid + 1;//条件不成立,第一个满足条件的元素>mid
}
return left;
}
int solve(int left,int right){左开右闭区间
int mid;
while(left + 1 < right){//left==right说明找到唯一的位置
mid = (left + right) / 2;//取中点
if(条件成立){//条件成立,第一个满足条件的元素的位置<=mid
right = mid;
}
else left = mid;//条件不成立,第一个满足条件的元素>mid
}
return right;
}
2.二分法拓展
给定一个定义在[L,R]上的单调函数f(x),求方程f(x)=0的根。
如果f(mid)>0说明根应该在子区间[left,mid]中
如果f(mid)<0说明根应该在子区间[mid,right]中
例如计算根号2的近似值等价于求f(x)=x^2-2=0在[1,2]范围内的根。
代码如下:
const int eps = 1e-5;//精度10^-5
double f(double x) {
return x * x - 2;
}
double solve(double L, double R) {
double left = L, right = R, mid;
while (right - left > eps) {
mid = (left + right) / 2;
if (f(mid) > 0)right = mid;
else mid = left;
}
return mid;
}