二分法
先说一个东西,确定左右边界的时候,有个技巧目前一直遵守:
就是
if () {
L = mid + 1;
}else(){
R = mid - 1;
}
大概意思就是,L与R,一个是 (mid + 1) 时,如果else/else if/if条件中是确定另一个边界,则另一边界值为 (mid - 1)–> 可直接写。
- 基本二分法 (加入对数器验证)
import java.util.Arrays;
public class BinarySearch {
public static boolean binarySearch(int[] arr, int target) {
if (arr == null || arr.length == 0) {
return false;
}
int L = 0;
int R = arr.length - 1;
while (L <= R) {
int mid = (L + R) / 2;
if (arr[mid] == target) {
return true;
} else if (arr[mid] < target) {
L = mid + 1;
} else {
R = mid - 1;
}
}
return false;
}
// 对数器
public static boolean test(int[] arr, int target) {
for (int num : arr) {
if (num == target) {
return true;
}
}
return false;
}
// 随机数组生成器
public static int[] lenRandomValueRandom(int maxLen, int maxValue) {
int len = (int) (Math.random() * maxLen); // int [0, maxLen - 1]
int[] arr = new int[len];
for (int i = 0; i < len; i++) {
arr[i] = (int) (Math.random() * maxValue); // int [0, maxValue - 1]
}
return arr;
}
public static int[] copyArray(int[] oldArr) {
int[] newArr = new int[oldArr.length];
for (int i = 0; i < oldArr.length; i++) {
newArr[i] = oldArr[i];
}
return newArr;
}
public static void main(String[] args) {
int maxLen = 50;
int maxValue = 1000;
int testTimes = 10000;
// 开始实测
for (int i = 0; i < testTimes; i++) {
int[] arr = lenRandomValueRandom(maxLen, maxValue);
// 二分法检测 一定要保证 !!!数组已经排序!!!
Arrays.sort(arr);
int[] tmp = copyArray(arr);
int target = (int) (Math.random() * (maxValue + 1)) - (int) (Math.random() * maxValue);
if (binarySearch(arr, target) != test(arr, target)) {
System.out.println("结果有误");
for (int j = 0; j < tmp.length; j++) {
System.out.print(tmp[j] + " ");
}
System.out.println();
System.out.println("搜索数字为" + target);
break;
};
}
}
}
二分进阶
- 在有序数组中找到 >= num 最左的位置
- 在有序数组中找到 <= num 最右的位置
package class03;
import java.util.Arrays;
public class BSNearLeft {
// arr是有序的, 寻找 >= num 的最左位置
public static int bsNearLeft(int[] arr, int num) {
if (arr == null || arr.length == 0) {
// -1表示该数组无法返回所求条件 --> -1代表特殊情况
return -1;
}
int L = 0;
int R = arr.length - 1;
// Step 1: 初始化 存储的目标值 ans --> -1 表示不存在
int ans = -1;
while (L <= R) {
// Step 2: 更新 ans
int mid = (L + R) / 2;
if (arr[mid] >= num) {
ans = mid;
R = mid - 1;
} else { // arr[mid] < num
L = mid + 1;
}
}
// Step 3: 返回 ans
return ans;
}
// 返回 <= num 的最右, 前提:数组有序
public static int bsNearRight(int[] arr, int num) {
if (arr == null || arr.length == 0) {
return -1;
}
int L = 0;
int R = arr.length - 1;
int ans = -1;
while (L <= R) {
int mid = (L + R) / 2;
if (arr[mid] <= num) {
ans = mid;
L = mid + 1;
} else {
R = mid - 1;
}
}
return ans;
}
// 对数器 --> 确认结果的检查器 功能:检查 >= num 的最左
// 前提: arr是有序的
public static int checkNearLeft(int[] arr, int num) {
if (arr.length == 0) {
return -1;
}
int ans = -1;
for (int i = 0; i < arr.length; i++) {
if (arr[i] >= num) {
ans = i;
break;
}
}
return ans;
}
// 检查 <= num 的最右
public static int checkNearRight(int[] arr, int num) {
if (arr.length == 0) {
return -1;
}
int ans = -1;
for (int i = 0; i < arr.length; i++) {
if (arr[i] <= num) {
ans = i;
} else {
break;
}
}
return ans;
}
public static int[] randomArray(int maxLen, int maxValue) {
int len = (int) (Math.random() * maxLen);
int[] arr = new int[len];
for (int i = 0; i < arr.length; i++) {
arr[i] = (int) (Math.random() * maxValue);
}
return arr;
}
public static void main(String[] args) {
int maxLen = 20;
int maxValue = 10000;
int testTimes = 10000;
for (int i = 0; i < testTimes; i++) {
int[] arr = randomArray(maxLen, maxValue);
int num = (int) (Math.random() * (maxValue + 1)) - (int) (Math.random() * maxValue);
Arrays.sort(arr);
if (checkNearLeft(arr, num) != bsNearLeft(arr, num)) {
System.out.println(">=num最左 的 结果出错");
System.out.println(Arrays.toString(arr));
System.out.println(num);
System.out.println(checkNearLeft(arr, num));
System.out.println(bsNearLeft(arr, num));
break;
}
if (checkNearRight(arr, num) != bsNearRight(arr, num)) {
System.out.println("<=num最右 结果出错");
}
}
}
}
- 局部最小值问题 ----》 无序 ,相邻不等
前提: 数组无序,且任意两个相邻的位置 数值不相等(arr[i] != arr[i + 1], i [0, arr.length - 2])
局部最小的定义:
(1)左边界局部最小:arr[0] < arr[1];
(2)右边界局部最小:arr[N - 1] < arr[N -2];
(3)普遍(中间):arr[i - 1] > arr[i] < arr[i + 1];
(4) (可以理解为左边界左边或右边界右边 大小为无限大)此条可忽略,coding时 可利用这种思路
问题: 找到第一个局部最小值?
解决方法: 二分法
个人代码:
package class03;
import java.util.Arrays;
public class BSAwesome {
// arr 无序
// arr 相邻元素 不相等
// 功能: 找出arr中 任意局部最小的index索引
// 输入: 数组 ---> int[] arr
// 输出: 局部最小的元素的索引下标 ---> int类型
public static int oneMinIndex(int[] arr) {
// 特殊情况 ---> summary: 一般对 arr == null, length == 0, length == 1,进行特殊处理
if (arr == null || arr.length == 0) {
// 返回 -1 表示无目标元素
return -1;
}
int N = arr.length;
// 长度为 1 ---> 直接返回唯一元素下标 0
if (N == 1) {
return 0;
}
// 考虑 length == 2 的情况 ---> length >= 2时, 统一处理
//首先确定左右边界,是否为局部最小
if (arr[0] < arr[1]) {
return 0;
}
if (arr[N - 1] < arr[N - 2]) {
return N - 1;
}
// 如果边界都不为局部最小,则 [L, R]中,必有局部最小,因为(1)相邻元素不相等,必有一大一小
// (2) 画一下图就可知道,必有 先向下,再向上 的拐点
int L = 0;
int R = arr.length - 1;
int ans = -1;
// 开始二分法 ---> 为什么也可以考虑到二分,因为它也是在找数字,具体看solution
// 这里,我们要对 arr进行二分,对每个arr[mid] 进行局部最小的比较,注意 考虑边界导致的特殊情况
// 特殊处理理解: 数组中的任何一个元素,都有可能二分到,所以要考虑任何一个元素 是否出现越界的情况
while (L <= R) {
int mid = (L + R) / 2;
// 特殊情况要先处理
// 左边界
if (mid == 0) {
L = mid + 1;
// 一定要注意, 执行完之后,不添加continue 还会继续往后做,出现越界
continue;
}
if (mid == N - 1) {
R = mid - 1;
continue;
}
// 现在 出现的所有元素 都是有左右元素
if (arr[mid] < arr[mid + 1] && arr[mid] < arr[mid - 1]) {
ans = mid;
break;
} else if (arr[mid] > arr[mid - 1]) {
// 只要arr[mid] > arr[mid - 1], 那么[L, mid - 1]一定存在局部最小
R = mid - 1;
} else { // arr[mid] > arr[mid + 1]
L = mid + 1;
}
}
return ans;
}
// Start 对数器 ---> 自动生成随机数样本进行比对的机器
// 随机数组生成器 ---> 但是这次有要求, 无序->OK, 相邻不重复->(improve)修改下代码
public static int[] randomArray(int maxLen, int maxValue) {
// 生成数组
int len = (int) (Math.random() * maxLen);
int[] arr = new int[len];
// 开始赋值 , 首先遍历数组 然后赋值
// 首先特殊情况
if (len == 0) {
return arr;
}
// 开始正常情况, 首先赋值arr[0]
arr[0] = (int) (Math.random() * maxValue);
// 注意i 从 1 开始 ----> 然后赋值 arr[1] -> arr[N - 1]
for (int i = 1; i < arr.length; i++) {
do {
arr[i] = (int) (Math.random() * maxValue);
} while (arr[i] == arr[i - 1]);
}
return arr;
}
// 比对器 (两个方向 : 一个是生成正确结果,然后与测试函数进行比较; 另一个是将测试函数的结果作为参数,进行正误校验) ---->
public static boolean check(int[] arr, int minIndex) {
// 特殊情况处理
if (arr.length == 0) {
return minIndex == -1;
}
int left = minIndex - 1;
int right = minIndex + 1;
// 简化代码 --> boolean ---> (左边界结果 & 右边界结果) ---> 从而可以分开处理
boolean leftResult = left >= 0 ? arr[minIndex] < arr[left] : true;
// minIndex < arr.length - 1
boolean rightResult = right < arr.length ? arr[minIndex] < arr[right] : true;
return leftResult & rightResult;
}
public static void main(String[] args) {
int maxLen = 100;
int maxValue = 200;
int testTime = 10000;
System.out.println("测试开始");
for (int i = 0; i < testTime; i++) {
int[] arr = randomArray(maxLen, maxValue);
int ans = oneMinIndex(arr);
boolean succeed = check(arr, ans);
if (!succeed) {
Arrays.toString(arr);
System.out.println("测试失败");
return;
}
}
System.out.println("测试成功");
}
}
第二版本: 左神代码
package class03;
public class Code04_BSAwesome {
// arr 整体无序
// arr 相邻的数不相等!
public static int oneMinIndex(int[] arr) {
if (arr == null || arr.length == 0) {
return -1;
}
int N = arr.length;
if (N == 1) {
return 0;
}
if (arr[0] < arr[1]) {
return 0;
}
if (arr[N - 1] < arr[N - 2]) {
return N - 1;
}
int L = 0;
int R = N - 1;
// L...R 肯定有局部最小
while (L < R - 1) { // 确保有三个元素
int mid = (L + R) / 2;
if (arr[mid] < arr[mid - 1] && arr[mid] < arr[mid + 1]) {
return mid;
} else {
if (arr[mid] > arr[mid - 1]) {
R = mid - 1;
} else {
L = mid + 1;
}
}
}
return arr[L] < arr[R] ? L : R;
}
// 生成随机数组,且相邻数不相等
public static int[] randomArray(int maxLen, int maxValue) {
int len = (int) (Math.random() * maxLen);
int[] arr = new int[len];
if (len > 0) {
arr[0] = (int) (Math.random() * maxValue);
for (int i = 1; i < len; i++) {
do {
arr[i] = (int) (Math.random() * maxValue);
} while (arr[i] == arr[i - 1]);
}
}
return arr;
}
// 也用于测试
public static boolean check(int[] arr, int minIndex) {
if (arr.length == 0) {
return minIndex == -1;
}
int left = minIndex - 1;
int right = minIndex + 1;
boolean leftBigger = left >= 0 ? arr[left] > arr[minIndex] : true;
boolean rightBigger = right < arr.length ? arr[right] > arr[minIndex] : true;
return leftBigger && rightBigger;
}
public static void printArray(int[] arr) {
for (int num : arr) {
System.out.print(num + " ");
}
System.out.println();
}
public static void main(String[] args) {
int maxLen = 100;
int maxValue = 200;
int testTime = 1000000;
System.out.println("测试开始");
for (int i = 0; i < testTime; i++) {
int[] arr = randomArray(maxLen, maxValue);
int ans = oneMinIndex(arr);
if (!check(arr, ans)) {
printArray(arr);
System.out.println(ans);
break;
}
}
System.out.println("测试结束");
}
}
总结:
二分是不是一定要有序? 不是
在某一种标准下,比如局部最小问题, 只要你能保证有一侧肯定有,你就可以使用二分法。