整数二分
1、概述
注意:单调性一定可以二分,但是二分不一定需要单调性 即二分与单调性无关,二分与二段性有关 即我们可以找到分界点,使得以分界点所在的左右区间分别满足不同的性质
二分的核心:每一次在缩小区间的时候,选择一个答案所在的区间,当区间的长度等于1的时候,答案一定就在里面
二分的步骤:
- 第一步:找中间值
- 第二步:判断某个条件,然后选择一个答案所在的区间,缩小左区间或者右区间,最终当区间中只有一个数时,这个数就是答案
2、经典二分题
这道题是一道经典的二分问题,包含了两种不同的情况,即寻找左端点与右端点两种情况
这道题要求使用O(logn)的做法完成,所以就是要求我们使用二分查找去解决,
2.1第一个位置
找第一个位置
由上面的内容,单调性一定可以二分,由于数组是排好序的(从小到大),即数组具有单调性,所以一定可以使用二分,接下来我们需要找到分界点,使得两个区间一个区间满足要求,一个区间不满足要求即可
就此题来说,我们第一步先找的是元素的第一次出现的位置,根据单调性可以发现,假设我们要找k在数组中第一个出现的位置,那么根据坐标轴我们可以得到下面的性质,第一个K后面的数(不包括K)一定是小于K的,K后面的数(包括K)都是大于等于K的,
所以,我们的目的就是找到K这个点
根据二分的步骤
1、第一步找中点,这里我们将做左端点记为l,右端点记为r 中点就是 mid = (l+r)/2
2、第二步,这里我们判断什么呢?根据上面的图,我们此时需要找到的分界点是K这个分界点,所以我们判断每次取得中间值时候大于等于K,即当满足绿色的区间时,说明中间值一定在K的右边,那么我们找K的位置,就一定在中间值的左边,所以这时我们要缩小右区间,让r=mid
同理,当中间值小于K时,说明K一定在中间值的左边,且中间值可能就在A这点,我们需要缩小左区间,让l=mid+1,
最终l和r会重合,l的位置就在K所在的位置
代码
int l = 0;
int r = len - 1;
//找到第一个数
while (l < r) {
int mid = l + r >> 1;
//满足右区间,r=mid
if (nums[mid] >= k) r = mid;
else l = mid + 1;
}
2.2最后一个位置
找第一个位置找的是右端点,最后一个位置我们找左端点
同理,我们找到最后一个K所在位置满足的条件,K所在的位置的前的元素都是小于等于K的,K后面的元素都是大于K的
我们要找到K的位置,跟上面同理
- 当中间值小于等于K时,K一定在中间值的右边,此时
l=mid
- 当中间值大于K时,K一定在中间值的左边,此时
r=mid-1
注意:当l=mid时,此时计算mid需要l+r+1,即额外加1,,当某一次满足某个条件时,l和r相差1, 因为Java是向下取整,所以(l+r)/2等于l,如果这次的mid还满足l=mid的条件,此时l=mid,(l+r)/2等于l,就会一直是l和r,会死循环,不会退出,如果我们将l+r+1后除2(r和l相差1)此时是等于r的 下一次循环l=r,r=r,就会退出循环
l = 0;
r = len - 1;
int mid = l + r >> 1;
while (l < r) {
//当 l=mid时,mid需要l+r+1
mid = l + r + 1 >> 1;
if (nums[mid] <= k) l = mid;
else r = mid - 1;
}
3、二段性的二分
此题就是二分的二段性,即以第一个错误版本为分界点开始,前面的版本都是正确版本,后面的都是错误版本,此题我们可以找第一个错误版本(右端点)
,也可以找到最后一个正确版本(左端点)
,此题只需要找右端点即可
注意:
由于此题的左右端点数值较大,l+r时可能溢出,所以我们计算中间值时让左端点+(右端点-左端点)/2,这样防止溢出
int mid = l + ((r - l)/ 2);
4、不明显的二分
5、旋转排序数组中的最小值
浮点数二分
1、模板
浮点数二分的话不需要考虑区间+1,-1的问题,因为每次都能将区间缩小一半,所以每次分区间时直接将l=mid
,或者r=mid即可
,且这个终止条件可以认为当 l和r相差10的-6次方时
认为l是等于r的,直接跳出循环,具体的要看题目要求
如果题目要求保留n位小数 r-l一般需要设置大于1e-(n+2) 比如此题要求保留6位小数 所以需要r-l>1e-8
bool check(double x) {
/* ... */} // 检查x是否满足某种性质
double bsearch(double l, double r){
// eps 表示精度,取决于题目对精度的要求
final double eps = 1e-6;
while (r - l > eps){
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}
2、应用
给定一个浮点数 n,求它的三次方根
- −10000≤n≤10000
- 结果保留 6 位小数
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
double d = scanner.nextDouble();
double l = -10000;
double r = 10000;
//保留n位小数 r-l一般需要设置大于1e-(n+2) 比如此题要求保留6位小数 所以需要r-l>1e-8
while (r - l > 1e-8){
double mid = (l + r)/2;
if(mid * mid * mid > d) r = mid;
else l = mid;
}
System.out.printf("%.6f",l);
}
}