未来属于那些相信梦想,并愿意为之付诸行动的人。
第一个大于等于X的位置(lower_bound/upper_bound)
前言
整数二分法
为了更好的理解这个查询过程,接下来我们来模拟这个二分查找过程.
指定的数字(binary_search)
现在我们需要从序列A={1,5,6,8,9,11,15,16,20}中查找数字11的位置,其中序列是0下标是0到8(下标1到9也是没问题的大家下去可以模拟一下)
1.[left,right]=[0,8],因此下标中点是mid=(left+right)/2=4;A[mid]=A[4]=9;9<11;说明需要在 [mid+1,right]范围内继续找,因此left=mid+1=5;
2.[left,right]=[5,8],因此下标中点是mid=(left+right)/2=6;A[mid]=A[6]=15;15>11;说明需要在[left,mid-1]范围内继续查找,因此right=mid-1=5;
3.[left,right]=[5,5],因此下标中点mid=(left+right)/2=5;A[mid]=A[5]=11;11=11;说明找到了需要查找的数X,返回下标5
ok,有了上面的基础下面我们来写代码吧。
int binarySearch(int A[],int left,int right,int x)
{
int mid;//mid为left和right的中点
while(left<=right)
{
mid=(left+right)/2; //取中点,这里一般写成left+(right-left)/2;避免溢出
if(A[mid]==x) return mid;//找到x,返回下标
else if(A[mid]>x) right=mid-1;//往左子区间[left,mid-1]查找
else left=mid+1; //往右子区间[mid+1,right]查找
}
return -1;//查找失败返回-1
}
如果序列是递减的,只需要把A[mid]>x改为A[mid]<x
二分法也可以用递归形式进行
//二分区间为左闭右闭的[left,right],传入的初值为[0,n-1]
int binarySearch(int* a, int left, int right, int key)
{
while (left < right)
{
int mid = left + ((right - left) >> 1);
int number = a[mid];
if (number < key)
{
return binarySearch(a, mid + 1, right, key);
}
else if (number > key)
{
return binarySearch(a, left, mid - 1, key);
}
else
return mid;
}
}
这里了解了,我们接下来要更进一步的讨论:如果递增序列A中的元素可能重复,那么如何对给定的欲查询元素x,求出序列中第一个大于等于x的元素的位置L以及不大于X的最后一个位置?
第一个大于等于X的位置(lower_bound/upper_bound)
做法其实和前面的很类似,下面我们来分析一下,假设当前的二分区间为左闭右闭区间[left,right],那么可以根据mid位置处的元素与欲查询元素x的大小来判断应当往哪个区间查找:
代码:
//A[]为递增,x为查询数字;
//二分上下界为左闭右闭的[left,right],传入的初值为[0,n]
int low_bound(int A[],int left,int right,int x)
{
int mid;
while(left<right)
{
mid=(left+right)/2;
if(A[mid]>=x) right=mid;//中间的数大于等于x,往左边区间找[left,mid]
else left=mid+1;//中间的数小于x,往右边区间找[mid+1,right]
}
return left;
}
拓展: 其实这就是C++中lower_bound()函数的用法,返回第一个大于等于x数的位置,不存在则返回最后一个数的下一个位置;对应的也有个upper_bound()函数的用法,它是返回第一个大于x数的位置;这个大家可以去实现下,其实就是A[mid]>=x换成A[mid]>x就可以了.这两个函数大家可以去了解下,具体实现上面其实已经介绍了.
不大于X的最后一个位置
这个实现就比上面的稍微难一点了.所以我这里需要引入个例题:
数的范围
简述下题意就是:就是查询一个数的起始位置和终止位置,不存在这个数就返回-1,所以这里取值范围不需要向上面一样取到n(这里不需要输出最后一个数的下一个位置);
需要写两个二分,一个需要找到>=x的第一个数,另一个需要找到<=x的最后一个数。查找不小于x的第一个位置,较为简单(上面已经讲过了):直接放代码
int l = 0, r = n - 1;
while (l < r) {
int mid = l + r >> 1;
if (a[mid] < x) l = mid + 1;
else r = mid;
}
查找不大于x的最后一个位置,便不容易了:
int l1 = l, r1 = n;
while (l1 + 1 < r1) {
int mid = l1 + r1 >> 1;
if (a[mid] <= x) l1 = mid;
else r1 = mid;
}
这里我看到了一位小哥写的解释,超级清楚,拿过来给大家看一下:AcWing 789. 数的范围(详细分析二分过程) - AcWing(大家也可以去这看)
int l = 0, r = n - 1;
while (l < r)
{
int mid = l + r + 1 >> 1;
if (a[mid] <= x) l = mid;
else r = mid - 1;
}
到这里核心的代码已经展示完毕,大家可以下去自己去做做.
整数二分模板
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
浮点数二分法
首先介绍如何计算根号2的近似值。
对于f(x)=x^2来说,在x属于[1,2]范围内,f(x)是随着x的增大而增大的,这就给使用二分法创造了条件,即可以用如下策略来逼近根号二的值。(这里不妨以精确到10^-5为例)
令浮点数left和right的初值分别是1和2,然后根据left和right的中点mid处f(x)的值与2的大小来选择子区间进行逼近.
上面两个步骤当right-left<10^-5时结束.
代码:
const double eps=1e-5;
double f(double x)
{
return x*x;
}
double calsqrt()
{
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;
}
浮点数二分法模板
bool check(double x) {/* ... */} // 检查x是否满足某种性质
double bsearch_3(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
总结: