文章目录
二分查找
二分查找也叫折半查找,在一个有序(递增或者递减)
的数列里寻找一个满足要求的数字
做法
如果我们要在一个有单调性的数组里查找某个数target,可以通过当前区间的最左边位置 left 和最右边位置right计算出中间位置 mid,再拿mid位置的元素和要找的元素 target 元素比较大小,来判断元素在在 mid 的左边还是右边,一次砍一半,重复此操作,找到可能的元素或者没有找到
下标问题
我们最直观的计算mid下标就是 通过 (left+right)/2,那么是否要考虑奇数和偶数的问题呢?
因为计算机在计算整数除法的时候是向下取整的 (1+3)/2 == (1+4)/2 的,无论是
边界问题
因为数组下标是从0开始的,为了方便计算mid下标,我们一般采取左闭右开区间,[left,right]
left 一般从0开始,不要管,
关键就是 right 的取值就关系到边界问题,也就是二分的结束条件
如果 right一般取的是数组最后一个下标,也就是数组长度 length-1
那么 二分的结束条件就是 left<=right
,如果不取等于就会出现结果被忽略的情况
图解
代码实现
注意:((right-left)>>1)+left 等价于 (left+right)/2
递归
public static boolean dichotomy(int[] arr,int left,int right,int target) {
// 边界判断
if (left > right) {
return false;
}
int mid = ((right-left)>>1)+left;
if (arr[mid] > target) {
// 如果中间位置的数比要找的数还要大,说明要找的数组在左半部分
return dichotomy(arr,left,mid-1,target);
} else if (arr[mid] < target) {
// 如果中间位置的数比要找的数字还要小,说明要找的数在数组的右半部分
return dichotomy(arr, mid+1, right, target);
} else {
return true;
}
}
迭代
public static boolean dichotomy(int[] arr,int target) {
int left = 0;
int right = arr.length-1;
int mid = 0;
while (left <= right) {
// 计算中间下标
mid = ((right-left)>>1) + left;
if (arr[mid] > target) {
// 如果中间位置的数比要找的数还要大,说明要找的数组在左半部分
right = mid-1;
} else if (arr[mid] < target) {
// 如果中间位置的数比要找的数字还要小,说明要找的数在数组的右半部分
left = mid+1;
} else {
return true;
}
}
// 如果没有找到
return false;
}
复杂度分析
因为二分查找都是每次将区间长度砍半,就是是每次区间都严格缩小一半,最差情况是区间长度变为零(找不到的情况)
n => n 2 = > n 4 = > n 8 . . . 停 止 条 件 → n 2 x , 2 x ≥ n \large\frac{n}{2} => \large\frac{n}{4} =>\large\frac{n}{8} ... \overrightarrow{停止条件} \frac{n}{2^{x}} ,2^{x} \ge n 2n=>4n=>8n...停止条件2xn,2x≥n
根据停止条件 2 x ≥ n 得 到 x = l o g n 2^x \geq n得到 x = log\ n 2x≥n得到x=log n
算法运行次数为 x x x,推出时间复杂度为 O ( x ) = O ( l o g n ) O(x) = O(log\ n) O(x)=O(log n)
二分查找变形
1. 求满足条件的最小值(后缀)
给定一个有单调性的数组 arr,问你大于等于 x x x的最小值是多少(不存在输出No)?
思路:
因为这是一个单调序列,那么满足大于等于 x x x条件的数一定是一个连续的区间,并且是从某一个位置一直往后(后缀),然后要找的就是这个后缀的最左边的值,就是大于等于 x x x的最小值。
假设当前的区间是 [ l , r ] [l,r] [l,r]
- 当 x ≤ a r r [ m i d ] x \le arr[mid] x≤arr[mid],可行区间缩减成 [ l , m i d − 1 ] [l,mid-1] [l,mid−1]
- 当 x > a r r [ m i d ] x > arr[mid] x>arr[mid],则可行区间缩减成 [ m i d + 1 , r ] [mid+1,r] [mid+1,r]
- 二分的条件是 l < = r l <= r l<=r
代码
public static void find(int[] arr, int x) {
int left = 0;
int right = arr.length-1;
int mid = 0;
while (left <= right) {
mid = ((right-left)>>1)+left;
if (arr[mid] >= x) {
right = mid-1;
} else {
left = mid+1;
}
}
if (left >= arr.length) {
System.out.println("不存在大于等于"+x+"数");
} else {
System.out.println(arr[left]);
}
}
假设序列是 [ 1 , 2 , 4 , 7 , 9 ] [1,2,4,7,9] [1,2,4,7,9],我们要找大于等于 3的最小数字
结论:最后的 l l l一定停留在这个后缀的最左边的位置
2. 求满足条件的最大值(前缀)
给定一个有单调性的数组arr,问你小于等于 x x x的最大值是多少?
思路:
和上一题类似,这也是一个单调序列,小于等于 x x x的数也是一个连续的区间,这里是从某一个位置往前(前缀)的数字一定会小于等于x
假设区间是 [ l , r ] [l,r] [l,r]
- 如果 a r r [ m i d ] ≤ x , l = m i d + 1 arr[mid]\le x, l = mid+1 arr[mid]≤x,l=mid+1
- 如 果 a r r [ m i d ] > x , r = m i d − 1 如果arr[mid] > x, r = mid-1 如果arr[mid]>x,r=mid−1
- 二分的循环条件 l ≤ r l \le r l≤r
代码
public static void find(int[] arr,int x) {
int left = 0;
int right = arr.length-1;
int mid = 0;
while (left <= right) {
mid = ((right-left)>>1)+left;
if (arr[mid] <= x) {
left = mid + 1;
} else {
right = mid - 1;
}
}
if (right < 0) {
System.out.println("不存在小于等于"+x+"的数");
} else {
System.out.println(arr[right]);
}
}
假设序列是 [ 1 , 2 , 4 , 7 , 9 ] , 我 们 要 找 小 于 等 于 5 的 最 大 值 [1,2,4,7,9],我们要找小于等于5的最大值 [1,2,4,7,9],我们要找小于等于5的最大值
结论 :最后 r r r 一定停留在 前缀的最右边,也就是小于等于 x x x最大值
3. 求最短子序列
给定一个正整数序列,让你取一个子段,使得其区间的和大于等于 x x x,问你这个子段最短可能长度是多少。
例如:[1,2,4,7,9] ,给定 x = 13 时应该得到 2, x = 3 时应该得到1,x 大于 23时应该不存在
暴力代码
从 1、1+2、1+2+3、1+2…再到 2、2+4/2+4+7…枚举所有可能,当大于等于x时就break,因为前面的区间已经满足了条件,就是最短序列了没要继续往后
public static void shortestSubsequence(int[] arr, int x) {
int min = Integer.MAX_VALUE;
for (int i = 0; i < arr.length; i++) {
int sum = 0;
for (int j = i; j < arr.length; j++) {
sum += arr[j];
if (sum >= x) {
min = Math.min(j-i+1,min);
break;
}
}
}
if (min != Integer.MAX_VALUE) {
System.out.println(min);
} else {
System.out.println("不存在");
}
}
小结
1.发现单调性: 固定左端点后,右端点越远,则区间的和越大。
2.做法:枚举左端点,二分找最近的右端点使得其大于等于 x x x. 然后对所有左端点的答案取最小值。
3.预处理区间和:利用前缀和技巧。
4.复杂度: O ( n l o g n ) O(nlog n) O(nlogn)
4. 大于x的平方数
给定一个数 x x x,求解第一个大于 x x x的平方数
二分思想,后缀模型
public static void maxFind(int x) {
int left = 1;
int right = x;
int mid = 0;
while (left <= right) {
mid = ((right-left)>>1)+left;
if (mid*mid > x) {
right = mid-1;
} else {
left = mid+1;
}
}
System.out.println(left*left);
}
5.二分浮点数
给定一个数 x x x,求解 x \sqrt{x} x的精确值,误差小于 1 e − 5 1e-5 1e−5
技巧:在不好确定循环次数的时候,不妨可以固定循环的次数 T T T,只要 T ≥ l o g n T \geq log\ n T≥log n 即可.
当外层循环次数越多,精确度越高,根据题目来
public static void sqrt(int x) {
double left = 0;
double right = x;
double mid = 0;
for (int i = 0; i < 55 ; i++) {
mid = (right - left) / 2.0 + left;
if (mid * mid <= x) {
left = mid;
} else {
right = mid;
}
}
System.out.println(left);
}
二分答案
二分答案是二分的一种进阶思想,通过观察题目发现答案存在单调性,通过二分答案后检查答案是否符合要求
常规做法
- 发现答案存在单调性
- 二分答案
- 检查答案是否可行
引入题目
给定一个正整数序列,让你取一个子段,使得其区间的和大于等于 x x x,问你这个子段最短可能长度是多少。
前面做这到题目的时候是通过枚举长度,每次枚举 i i i 也就是长度,每次枚举后扫一遍序列,判断是否有一个长度为 i i i 的区间和 ≥ x \geq x ≥x,如果存在,则记录结果然后 break
新思路
-
单调性:我们知道,区间越大,它的区间和越大,所以具有单调性。最终的答案满足一下关系
在区间长度比较小的时候是不满足,在区间长度增加的过程中,知道某个分割点 S S S 使得它能够满足答案,这个位置就是我们希望找到的点,这个问题可以用二分解决
-
二分可能的长度 x x x
-
检查:对对于一个长度为 x x x的序列,如何检查这个答案可行?
枚举所有可能的长度为x的字段(可以按右端点枚举),对他们的区间和之间取最大值,看是否大于等于 x x x
代码
/**
* 判断是否存在一个长度为 len 的区间,它的和是否大于等于 x
* @param array 序列
* @param len 区间长度
* @param x
* @return
*/
public static boolean flag(int[] array, int len, int x) {
int max = 0;
for (int i = len; i < array.length; i++) {
// 枚举所有可能的区间,在它们的区间和之中取最大值,看看是否大于等于x
max = Math.max(max,array[i]-array[i-len]);
}
// 判断和是否大于等于 x
return max >= x;
}
public static void binarySequence(int[] arr,int x) {
int[] array = new int[arr.length+1];
//计算前缀和
for (int i = 1; i < array.length; i++) {
array[i] = array[i-1]+arr[i-1];
}
// System.out.println(Arrays.toString(arr));
// System.out.println(Arrays.toString(array));
//二分长度
int left = 1;
int right = arr.length;
int mid = 0;
while (left <= right) {
mid = ((right-left)>>1)+left;
if (flag(array,mid,x)) {
right = mid-1;
} else {
left = mid+1;
}
}
if (left >= array.length) {
System.out.println("不存在序列和大于等于"+x);
} else {
System.out.println(left);
}
}
复杂度
n log 2 n n \log_{2}{n} nlog2n
小结
直观来讲,二分答案就是一种(因为答案有单调性,所以)利用二分思想优化枚举答案过程的算法