- 经常见到的类型是在一个有序数组上,开展二分搜索,但有序真的是所有问题求解时使用二分的必要条件吗?
- 不
,只要能正确构建左右两侧的淘汰逻辑,你就可以二分**。**
- 边界条件需要注意,可能会写出错的边界
- 经典案例:二分法查找值===>有序,一次砍一半
- 复杂度:O(log2N)===>一次砍一半,一共砍log2N次,每找到一次看一眼决定怎么砍的都是O(1)的
- 以10为底lg,以e为底ln,以2为底log,以其他为底logx
- 长度非2的整次方时也是O(log2N),因为复杂度忽略常数项……
- 有序的一定能二分,无序的也有可能能二分
经典题型
- 在一个有序数组中,找某个数是否存在
- 在一个有序数组中,找>=某个数最左侧的位置
- 二分到最后,一定是到没有数的
- 砍一半,砍掉的一半知道是否满足条件,但是另一半不知道
- 一定会二分到死,到最后,在这过程中用一变量记录着出现的答案,最后一和答案就是最终答案
- 在一个有序数组中,找<=某个数最右侧的位置
- 局部最小值问题
- 无序数组(包含正负和0)
- 任意相邻的数不相等
- 0位置只要小于1位置就是局部最小
- n-1位置只要小于n-2位置就是局部最小
- 其他位置n不仅要比n-1小还要比n+1小才是局部最小
- 在无序数组中只要找到这样一个局部最小就行(不管哪一个、有多少),只要找一个局部最小的位置即可,返回一个即可(不管返回哪一个)
- 二分不一定要有序才能二分(见4)
- 数据状况特殊的情况
- 问的问题本身很特殊—->局部极小值
- 满足上述两个条件可能有特殊的优化
- 优化角度
- 数据本身
- 问的问题
- 只要构建一种排他性的策略,左右两侧有一半肯定有,另一半可能有或可能没有,将那一半不确定的砍掉即可,留下肯定有的那一半
- 只要能构建排他性的条件(东西),不一定数组有序,题目就能二分!
- 二分的循环条件中是小于的时候表示至少要两个数(最后一次left等于了right,所以最后还要判断left是不是满足条件或者直接返回left;注意:放缩即左右选取是在while循环体内进行的,通过判断选择砍哪一半,直到最后砍到只剩一个元素为止直接跳出,而下面一种方式是将最后一个元素依然包括在循环体内判断然后直接返回的),二分的循环条件中是小于等于的时候表示至少一个数(这时所有的判断都在while循环以内,直接return结果)
一个有序数组中,找某个数是否存在
package class01;
public class Code04_BSExist {
public static boolean exist(int[] sortedArr, int num) {
if (sortedArr == null || sortedArr.length == 0) {
return false;
}
int L = 0;
int R = sortedArr.length - 1;
int mid = 0;
// L..R
// L~R上至少有两个数时才进行二分,所以退出的时候可能还有一个数没有验,所以要把这最后一个数验一下
while (L < R) {
// mid = (L+R) / 2;
// L 10亿 R 18亿
// 数值太大了会造成溢出,会出错!、
// 使用这种写法可以避免溢出
// 相减不会溢出,除以更不会溢出;并且R没有溢出,加上相减的一半也不会溢出
// l/2+r/2应该也可以,但两次除效率低;在都是奇数的情况下要处理好===>边界条件调整好可以搞
// l/2不是裸的l/2,出来后可能要加工一下(加1、减1这样)
// N/2 = N>>1(带符号右移)
// 位运算比除法、乘法快
// 能优化一点小常数就优化一点小常数
// mid = L + (R - L) / 2
// N / 2 N >> 1
mid = L + ((R - L) >> 1); // mid = (L + R) / 2
if (sortedArr[mid] == num) {
return true;
} else if (sortedArr[mid] > num) {
R = mid - 1;
} else {
L = mid + 1;
}
}
// L~R上至少有两个数时才进行二分,所以退出的时候可能还有一个数没有验,所以要把这最后一个数验一下
// while (L < R) {}都对,只不过这两种情况下逻辑的边界条件不一样===>处理方式不一样但都对
// 边界条件必不可少,仁者见仁智者见智
return sortedArr[L] == num;
}
}
在一个有序数组中,找>=某个数最左侧的位置
- 二分到最后,二分到没有数的
- 图中t为最终答案,所以当中间位置不是大于等于3的时候,t不更新
- t是用来记录答案的,与mid中间位置无关!!!
- 思路二分:看mid所对应的数是不是大于等于那个数的
- 假如大于等于,记录下这时的mid,并且砍掉右半部分,留下左半部分,进行下一次二分,直到L>R
- 假如小于,保存最终答案的那个变量不更新,砍掉左半部分,留下右半部分,进行下一次二分,直到L>R
- 最终结果保存在变量t中
package class01;
import java.util.Arrays;
public class Code05_BSNearLeft {
// 在arr上,找满足>=value的最左位置
public static int nearestIndex(int[] arr, int value) {
int L = 0;
int R = arr.length - 1;
int index = -1; // 记录最左的对号
// 至少一个数时可以二分(注意边界条件)
while (L <= R) {
// 只剩一个数时,算mid的时候仍然是mid=L=R(此时L或者R无论是谁再变化一下都会造成L>R跳出循环)
int mid = L + ((R - L) >> 1);
if (arr[mid] >= value) {
index = mid;
R = mid - 1;
} else {
L = mid + 1;
}
}
// 返回结果
return index;
}
// for test
public static int test(int[] arr, int value) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] >= value) {
return i;
}
}
return -1;
}
// for test
public static int[] generateRandomArray(int maxSize, int maxValue) {
int[] arr = new int[(int) ((maxSize + 1) * Math.random())];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
}
return arr;
}
// for test
public static void printArray(int[] arr) {
if (arr == null) {
return;
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
System.out.println();
}
public static void main(String[] args) {
int testTime = 500000;
int maxSize = 10;
int maxValue = 100;
boolean succeed = true;
for (int i = 0; i < testTime; i++) {
int[] arr = generateRandomArray(maxSize, maxValue);
Arrays.sort(arr);
int value = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random());
if (test(arr, value) != nearestIndex(arr, value)) {
printArray(arr);
System.out.println(value);
System.out.println(test(arr, value));
System.out.println(nearestIndex(arr, value));
succeed = false;
break;
}
}
System.out.println(succeed ? "Nice!" : "Fucking fucked!");
}
}
一个有序数组中,找<=某个数最右侧的位置
局部最小值问题
- 单独看0位置是不是小于1位置,是就是局部最小
- 单独看n-1位置是不是小于n-2位置,是就是局部最小
- 假如上面两个都没有满足那么趋势是这样的情况 \…………/ ,中间怎么变化不知道,但必然有局部最小值
- 先看中点位置,假如mid两边都比mid大,那么返回mid
- 如果不成立(从1到n-2二分)
- 假如mid的左边比mid小,而右边比mid大,那么 \……/mid/……/ ,则左侧必然存在局部最小,右边不确定,砍掉右侧,保留左侧
- 假如mid的左边比mid大,而右边比mid小,那么 \……\mid\……/ ,则右侧必然存在局部最小,左边不确定,砍掉左侧,保留右侧
- 假如mid的左边比mid大且右边比mid大,那么 \……/mid\……/ ,则两侧都必然存在局部最小,这时随便砍掉一侧,保留一侧即可
- 假如mid的左边比mid小,而右边比mid小,那么 \……\mid/……/ ,则mid就是局部最小值,此时返回mid即得结果(下图中的高度代表大小)
- 左右都小时(即mid比左右都大时),选左选右都可以,只要找到一个即可,不需要判断谁更小了
package class01;
public class Code06_BSAwesome {
public static int getLessIndex(int[] arr) {
if (arr == null || arr.length == 0) {
return -1; // no exist
}
if (arr.length == 1 || arr[0] < arr[1]) {
return 0;
}
if (arr[arr.length - 1] < arr[arr.length - 2]) {
return arr.length - 1;
}
int left = 1;
int right = arr.length - 2;
int mid = 0;
while (left < right) {
mid = (left + right) / 2;
if (arr[mid] > arr[mid - 1]) {
// 左右都小和左小右大的情况走这个分支
right = mid - 1;
} else if (arr[mid] > arr[mid + 1]) {
// 左大右小的情况走这个分支
left = mid + 1;
} else {
// 左右都大时走这个分支,即找到局部最小值了===>结束
return mid;
}
}
return left;
}
}