二分查找法-java
概述
二分查找是一种思想,不是一种特定的算法。所以不要局限于在一个有序的数组中查找,二分查找的本质是将一个数组一分为 2,利用“排他性”(另外一半决定没有,或者不影响我查找的行为)来缩小查找范围,从而搜寻数据。
所以,我们可以利用二分查找的思想来查找一个无序数组的局部最小值(这个问题将在文章末尾给出解答)等其他问题。
经典实现
非递归方式
public static int binearSearch(int[] arr, int Key) {
if (arr.length < 2 || //数据校验,排除不合理输入
Key > arr[arr.length - 1] || Key < arr[0]) {
return -1;
}
int L = 0;
int R = arr.length - 1;
int M = -1;
while (L <= R) {
M = L + ((R - L) >> 1);
if (arr[M] == Key) {
return M;
} else if (arr[M] < Key) {
L = M + 1;
} else {
R = M - 1;
}
}
return -1;
}
Drawing 2022-07-14 12.54.18.excalidraw.png 流程演示
Tip:
为什么用 L + ((R - L) >> 1) 而不是 (L + R) / 2 代表中值?
- 位运算更偏向于硬件层面,效率更高;
- L+R 可能会溢出,而 R-L 则更不可能溢出;
递归方式
public static int binearSearch_recursion(int[] arr, int L, int R, int Key) {
if (L > R || arr.length < 2 ||
Key > arr[R] || Key < arr[L]) {
return -1;
}
int M = L + ((R - L) >> 1);
int index = -1;
if (arr[M] == Key) {
return M;
} else if (arr[M] < Key) {
index = binearSearch_recursion(arr, M + 1, R, Key);
} else {
index = binearSearch_recursion(arr, L, M - 1, Key);
}
return index;
}
注意: 这里 index 的作用是方便保存递归返回的下标的,为了使找到的下标顺利回到主栈。
总结
相信代码并不难看懂,无论是递归版的还是非递归版的,本质都是将数据一分为二,舍弃一部分数据。
二分查找变式
既然都是排除一些无用的数值,那么有没有其他“更高效”的排除方式呢?显然一种思路就是改二分法为其他的“分法”。
但不管是什么分法,其都是表现在 mid (中值)上的——以 mid 为分界,一边为有用数据,一边为无用数据。
插值查找法
下图是 mid 的修改逻辑:
!Pasted image 20220714131906.png
具体的数学原理涉及到数值分析的内容,所以这里就不加赘述。
右式大概表示了 key 值在数组中的大致位置,这使得一开始 mid 就与 key 比较接近,从而所需要比较的次数就少了。但这也是不稳定的,当这个数组越是接近等差数列,mid 就与 key 的实际位置越接近。所以,当数组越接近于线性,插值查找法的效率越高。
public static int binearSearch_insert(int[] arr, int Key) {
if (arr.length < 2 ||
Key > arr[arr.length - 1] || Key < arr[0]) {
return -1;
}
int L = 0;
int R = arr.length - 1;
int M = -1;
while (L < R) {
M = L + (R - L) *(Key-arr[L])/(arr[R]-arr[L]);//插值法核心
if (arr[M] == Key) {
return M;
} else if (arr[M] < Key) {
L = M + 1;
} else {
R = M - 1;
}
}
return -1;
}
疑问: 为什么这里的 while 循环中不是 L<=R,而是 L<R ?
斐波那契查找法
同理,斐波那契查找法只是将“二分”变为“黄金分割”。
public static final int MAXSIZE = 20;
public static int fibonacci_Search(int[] arr, int Key) {
int max = arr.length - 1;
int[] fib = fib();
int k = 0;
while (max > fib[k] - 1) { //获取合适大小的斐波那契数列
k++;
}
int[] temp = Arrays.copyOf(arr, fib[k]); //使arr的长度等于某一斐波那契数列的元素
for (int i = arr.length; i < temp.length; i++) { //将多余的位用最大值填满,以保证
temp[i] = arr[max]; //有序
}
int L = 0;
int R = max;
int M = -1;
while (L < R) {
M = L + fib[k - 1] - 1;
if (arr[M] == Key) {
return M;
} else if (arr[M] < Key) {
L = M + 1;
k -= 2;
} else {
R = M - 1;
k -= 1;
}
}
return L;
}
public static int[] fib() { //生成斐波那契数列
int[] f = new int[MAXSIZE];
f[0] = 1;
f[1] = 1;
for (int i = 2; i < f.length; i++) {
f[i] = f[i - 1] + f[i - 2];
}
return f;
}
解读: 上述代码中的斐波那契数组的作用是为了每次都让 mid 取到合适的值。比如说,第一次分割就让其取到 temp 数组长度的黄金分割比(0.618)的数据。第二次则从这 0.618 或 0.382 中继续分割 0.618 。
局部最值问题
最后,我们来完成开头提到的局部最值问题:
假设有一串无序数组,里面的数据相邻各不相同,求其中的一个局部最小值(局部最小值指这个值比其周围的数据(i-1 与 i+1)都要小)。
以下是代码实现:
public static int min_search(int[] arr) {
if (arr[arr.length - 1] < arr[arr.length - 2]) { //第一部分
return arr.length - 1;
}
if (arr[0] < arr[1]) {
return 0;
}
int L = 1;
int R = arr.length - 2;
int M = -1;
int index = -1;
while (L < R) { //第二部分
M = L + ((R - L) >> 1);
if (arr[M - 1] < arr[M]) {
R = M - 1;
} else if (arr[M] > arr[M + 1]) {
L = M + 1;
} else {
return M;
}
}
return L;
}
我们可以看到除了 while 循环内的 if 的判断条件略有不同,其余的部分几乎与二分查找一模一样。
- 第一部分代码是为了判断左右端点是不是局部最小值,如果不是再进行进一步判断;
- 当左右端点不是局部最小值时,那么就走向中点,判断中点满不满足条件;
- 如果中点不满足且中点比其左邻接的元素大,那么就意味着左侧必定有一个局部最小值(同理,右边也有),于是右边有没有局部最小值于我们而言是没有意义的,于是就可以舍弃右边的数据,继续在左侧查找;
- 重复取中点,直到找到局部最小值为止;