概述
二分查找算法,也称为折半查找算法,是一种常见的查找算法。该算法的基本思想是:将有序数组从中间分成两个部分,如果要查找的元素比中间元素大,则在右边继续查找;如果要查找的元素比中间元素小,则在左边继续查找。重复这个过程,直到找到目标元素或者确定目标元素不存在。
再次强调:使用二分查找的前提是该数组必须有序
基础版
下面来实现以下二分查找的基础代码:
/**
* <h3>二分查找基础版</h3>
*
* <ol>
* <li>i, j, m 指针都可能是查找目标</li>
* <li>因为 1. i > j 时表示区域内没有要找的了</li>
* <li>每次改变 i, j 边界时, m 已经比较过不是目标, 因此分别 m+1 m-1</li>
* <li>向左查找, 比较次数少, 向右查找, 比较次数多</li>
* </ol>
*
* @param a 待查找的升序数组
* @param target 待查找的目标值
* @return <p>找到则返回索引</p>
* <p>找不到返回 -1</p>
*/
public static int binarySearchBasic(int[] a, int target) {
int i = 0, j = a.length - 1; // 设置指针和初值
while (i <= j) { // i~j 范围内有元素
int m = (i + j) >>> 1; //int m = (i + j) /2;
if (target < a[m]) { // 目标在左边
j = m - 1;
} else if (a[m] < target) { // 目标在右边
i = m + 1;
} else { // 找到了
return m;
}
}
return -1;
}
为什么是 i<=j 意味着区间内有未比较的元素, 而不是 i<j ? i==j 意味着 i,j 它们指向的元素也会参与比较 i<j 只意味着 m 指向的元素参与比较
在设置中间元素m时,我们尽量写成右移1为进行定义,原因一是移位操作本质是对二进制位进行直接操作,没有复杂的运算逻辑,因此对比普通乘除法效率更高。原因二是Java会把二进制数的最高位判定为符号位,因此在进行两个较大的数相加时可能会超过范围会变成负数,无符号右移相当于除2后向下取整,右移就把原数字移动到java能表示的最大正数范围内了,但是除法运算不会动符号位,就会出现问题
下面为具体的循环过程:
改动版
在二分查找的基础版上进行改动,让原来指向最后元素的j指针作为边界条件,不参与查找的过程,我们就能减少比较的次数,让效率略微提高
/**
* <h3>二分查找改动版</h3>
*
* <ol>
* <li>i, m 指针可能是查找目标</li>
* <li>j 指针不可能是查找目标,它作为边界,不会参与比较</li>
* <li>因为 1. 2. i >= j 时表示区域内没有要找的了</li>
* <li>改变 i 边界时, m 已经比较过不是目标, 因此需要 i=m+1</li>
* <li>改变 j 边界时, m 已经比较过不是目标, 同时因为 2. 所以 j=m</li>
* </ol>
*
* @param a 待查找的升序数组
* @param target 待查找的目标值
* @return <p>找到则返回索引</p>
* <p>找不到返回 -1</p>
*/
public static int binarySearchAlternative(int[] a, int target) {
int i = 0, j = a.length; // 第一处
while (i < j) { // 第二处 若为<=,则会陷入死循环
int m = (i + j) >>> 1;
if (target < a[m]) {
j = m; // 第三处 j作为边界,不会参与比较,因此不是m-1
} else if (a[m] < target) {
i = m + 1;
} else {
return m;
}
}
return -1;
}
平衡版
显然,在改动版的基础上,我们会发现当待查找的目标在数组的最右侧时,每次对其查找都需要进行两次比较,而若在左侧,则仅需进入第一个if即可,这就导致了左右查找效率的不平衡,因此我们还要稍加改进
元素在最左边 L 次, 元素在最右边 2*L 次
/**
* <h3>二分查找平衡版</h3>
*
* <ol>
* <li>不奢望循环内通过 m 找出目标, 缩小区间直至剩 1 个, 剩下的这个可能就是要找的(通过 i)</li>
* <li>i 指针可能是查找目标</li>
* <li>j 指针不可能是查找目标</li>
* <li>因为 1. 2. 3. 当区域内还剩一个元素时, 表示为 j - i == 1</li>
* <li>改变 i 边界时, m 可能就是目标, 同时因为 2. 所以有 i=m</li>
* <li>改变 j 边界时, m 已经比较过不是目标, 同时因为 3. 所以有 j=m</li>
* <li>三分支改为二分支, 循环内比较次数减少</li>
* </ol>
*
* @param a 待查找的升序数组
* @param target 待查找的目标值
* @return <p>找到则返回索引</p>
* <p>找不到返回 -1</p>
*/
public static int binarySearchBalance(int[] a, int target) {
int i = 0, j = a.length;
while (1 < j - i) { // 范围内待查找的元素个数 > 1 时
int m = (i + j) >>> 1;
if (target < a[m]) { // 目标在左边
j = m;
} else { // 目标在 m 或右边
i = m;
}
}
return (target == a[i]) ? i : -1;
}
优先级版
我们还会发现,实际情况可能会出现数组中有两个元素的值相等,那么如果我们待查找的是这个元素值,则该返回的应该是左边的还是右边的呢?
对左和右谁先再完善:
左先:
/**
* <h3>二分查找 Leftmost </h3>
*
* @param a 待查找的升序数组
* @param target 待查找的目标值
* @return <p>返回 ≥ target 的最靠左索引</p>
*/
public static int binarySearchLeftmost2(int[] a, int target) {
int i = 0, j = a.length - 1;
while (i <= j) {
int m = (i + j) >>> 1;
if (target <= a[m]) {
j = m - 1;
} else {
i = m + 1;
}
}
return i;
}
右先:
/**
* <h3>二分查找 Rightmost </h3>
*
* @param a 待查找的升序数组
* @param target 待查找的目标值
* @return <p>返回 ≤ target 的最靠右索引</p>
*/
public static int binarySearchRightmost2(int[] a, int target) {
int i = 0, j = a.length - 1;
while (i <= j) {
int m = (i + j) >>> 1;
if (target < a[m]) {
j = m - 1;
} else {
i = m + 1;
}
}
return i - 1;
}
递归版
此外,递归也是实现二分查找的一个好方法,二分查找还可以用单路递归进行实现
/**
* 递归二分查找
*/
public class E03BinarySearch {
public static int search(int[] a, int target) {
return f(a, target, 0, a.length - 1);
}
/**
* <h3>递归(子问题)函数:查找 [i .. j] 范围内的目标</h3>
*
* @param a 数组
* @param target 待查找值
* @param i 起始索引
* @param j 结束索引
* @return <p>找到返回索引</p><p>找不到返回 -1</p>
*/
private static int f(int[] a, int target, int i, int j) {
if (i > j) {
return -1;
}
int m = (i + j) >>> 1;
if (target < a[m]) {
return f(a, target, i, m - 1);
} else if (a[m] < target) {
return f(a, target, m + 1, j);
} else {
return m;
}
}
}