二分查找(Java 版)
本文是二分查找算法的总结归纳
更多请参照算法刷题套路和模板的GitHub仓库
文章目录
简介
二分查找也称折半查找(Binary Search
),它是一种效率较高的查找方法。但是,二分查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。
二分查找算法是典型的「减治思想」的应用,我们使用二分查找将待查找的区间逐渐缩小,以达到「缩减问题规模」的目的。
比如查找升序数组nums
里的目标值target
(这里只讨论升序数组,降序数组是一样的道理):
int [] nums = { 0, 1, 2, 3, 4, 5, 6, 7 };
约定
我们把待查找区间的左边界下标设为left
,右边界下标设为right
,中间位置下标设为mid
。
// 待查找区间的左边界下标
int left = 0;
// 待查找区间的右边界下标
int right = a.length - 1;
// 中间位置下标取法有两种
// 下取整,不写成 (right + left) / 2 是为了防止溢出
int mid = left + (right - left) / 2;
// 上取整
int mid = left + (right - left + 1) / 2;
// 中间位置下标求值的优化
// 1.使用右移位运算符,右移 1 位相当于除以 2
// 下取整
int mid = left + (right - left) >> 1;
// 上取整
int mid = left + (right - left + 1) >> 1;
// 2.使用无符号右移位运算符(貌似仅 Java 有),参考 JDK 源码 Arrays.binarySearch() 的写法,
// left + right 即使是在整型溢出以后,仍然能够得到正确的结果
// 下取整
int mid = (right + left) >>> 1;
// 上取整
int mid = (right + left + 1) >>> 1;
打印数组的共通ArrayUtil
public class ArrayUtil {
/**
* 打印整型数组
* @param arrays
*/
public static void printArray(int[] arrays) {
StringBuilder sBuilder = new StringBuilder();
sBuilder.append("{ ");
for (int i : arrays) {
sBuilder.append(i + ", ");
}
// 删除多余的", "
sBuilder.delete(sBuilder.length() - 2, sBuilder.length());
sBuilder.append(" }");
System.out.println(sBuilder);
}
}
一、模板 1:while (left <= right)
1、思路:在循环体内部查找元素(解决简单问题时有用),即考虑下一轮目标元素应该在哪个区间
把待查找区间[left, right]
分为 3 个部分:
mid
位置(只有 1 个元素);[left, mid - 1]
里的所有元素;[mid + 1, right]
里的所有元素;
于是,二分查找就是不断地在区间[left, right]
里根据中间元素nums[mid]
与target
的大小关系来不断缩小查找区间,最终找到target
的下标:
nums[mid] == target
时,返回mid
;nums[mid] > target
时,由于数组升序,mid
以及mid
右边的所有元素都大于target
,下一轮目标元素一定在区间[left, mid - 1]
里,因此设置right = mid - 1
;nums[mid] < target
时,由于数组升序,mid
以及mid
左边的所有元素都小于target
,下一轮目标元素一定在区间[mid + 1, right]
里,因此设置left = mid + 1
。
2、图解
3、代码实现
public class BinarySearch {
public static void main(String[] args) {
int[] nums = { 0, 1, 2, 3, 4, 5, 6, 7 };
int target = 2;
System.out.print("数组 nums:");
ArrayUtil.printArray(nums);
System.out.println("目标值 target:" + target);
System.out.println("模板 1 下标:" + binarySearch1(nums, target));
}
/**
* 二分查找法<br>
* <li>模板 1:while (left <= right)</li><br>
* @param nums 待查找数组
* @param target 待查找目标值
* @return 目标值在数组中的下标<br>
* 未查找到就返回 -1
*/
public static int binarySearch1(int[] nums, int target) {
// 特殊用例判断
int len = nums.length;
if (len == 0) {
return -1;
}
// 在 [left, right] 区间里查找 target
int left = 0;
int right = len - 1;
while (left <= right) {
// 为了防止 left + right 整形溢出,写成如下形式
int mid = left + (right - left) / 2;
// 找到目标值,直接返回
if (nums[mid] == target) {
return mid;
} else if (nums[mid] > target) {
// [mid, right]都大于目标值,下一轮查找区间:[left, mid - 1]
right = mid - 1;
} else {
// 此时:nums[mid] < target
// [left, mid]都小于目标值,下一轮查找区间:[mid + 1, right]
left = mid + 1;
}
}
return -1;
}
}
运行结果:
数组 nums:{ 0, 1, 2, 3, 4, 5, 6, 7 }
目标值 target:2
模板 1 下标:2
二、模板 2:while (left < right),推荐使用
1、思路:在循环体内部排除元素(解决复杂问题时非常有用),即考虑中间元素 nums[mid] 在什么情况下不是目标元素
把待查找区间[left, right]
分为 2 个部分:
- 不存在目标元素(
if
分支); - 可能存在目标元素(
else
分支,包含mid
);
与模版 1 同样,二分查找就是不断地在区间[left, right]
里根据中间元素nums[mid]
与target
的大小关系来不断缩小查找区间,最终找到target
的下标:
①、中间位置下取整
nums[mid] < target
时,mid
以及mid
左边元素都小于target
,下一轮目标元素一定在区间[mid + 1, right]
里,因此设置left = mid + 1
。
②、中间位置上取整
nums[mid] > target
时,mid
以及mid
右边元素都小于target
,下一轮目标元素一定在区间[left, mid - 1]
里,因此设置right = mid - 1
;
Tips:先写if else
分支,再决定是中间位置是上取整(target
在左边)还是下取整(target
在右边)。
特征:
while (left < right)
,这里使用严格小于<
表示的临界条件是:当区间里的元素只有 2 个时,依然可以执行循环体。换句话说,退出循环的时候一定有left == right
成立,这一点在定位元素下标的时候极其有用。
2、图解
3、代码实现
public class BinarySearch {
public static void main(String[] args) {
int[] nums = { 0, 1, 2, 3, 4, 5, 6, 7 };
int target = 2;
System.out.print("数组 nums:");
ArrayUtil.printArray(nums);
System.out.println("目标值 target:" + target);
System.out.println("模板 2(下取整)下标:" + binarySearch2_floor(nums, target));
System.out.println("模板 2(上取整)下标:" + binarySearch2_ceil(nums, target));
}
/**
* 二分查找法<br>
* <li>模板 2(下取整):while (left < right)</li><br>
* @param nums 待查找数组
* @param target 待查找目标值
* @return 目标值在数组中的下标<br>
* 未查找到就返回 -1
*/
public static int binarySearch2_floor(int[] nums, int target) {
// 特殊用例判断
int len = nums.length;
if (len == 0) {
return -1;
}
// 在 [left, right] 区间里查找 target
int left = 0;
int right = len - 1;
while (left < right) {
// 选择中间位置时下取整
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
// [left, mid]都小于目标值,下一轮查找区间是 [mid + 1, right]
left = mid + 1;
} else {
// 此时:nums[mid] >= target
// [mid, right]都大于等于目标值,下一轮查找区间是 [left, mid]
right = mid;
}
}
// 退出循环的时候 left == right,程序只剩下一个元素没有看到。
// 视情况,是否需要单独判断 left(或者 right)这个下标的元素是否符合题意。
return nums[left] == target ? left : -1;
}
/**
* 二分查找法<br>
* <li>模板2(上取整):while (left < right)</li><br>
* @param nums 待查找数组
* @param target 待查找目标值
* @return 目标值在数组中的下标<br>
* 未查找到就返回 -1
*/
public static int binarySearch2_ceil(int[] nums, int target) {
// 特殊用例判断
int len = nums.length;
if (len == 0) {
return -1;
}
// 在 [left, right] 区间里查找 target
int left = 0;
int right = len - 1;
while (left < right) {
// 选择中间位置时上取整
int mid = left + (right - left + 1) / 2;
if (nums[mid] > target) {
// [mid, right]都大于目标值,下一轮查找区间是 [left, mid - 1]
right = mid - 1;
} else {
// nums[mid] <= target
// [left, mid]都小于等于目标值,下一轮查找区间是 [mid, right]
left = mid;
}
}
// 退出循环的时候 left == right,程序只剩下一个元素没有看到。
// 视情况,是否需要单独判断 left(或者 right)这个下标的元素是否符合题意。
return nums[left] == target ? left : -1;
}
}
运行结果:
数组 nums:{ 0, 1, 2, 3, 4, 5, 6, 7 }
目标值 target:2
模板 2(下取整)下标:2
模板 2(上取整)下标:2
三、模板 3:while (left + 1 < right)
如果已经掌握了模板 2,就无需掌握这个模板,仅作了解。
1、与模版 2 的区别
这一版代码和模板 2 没有本质区别,一个显著的标志是:循环可以继续的条件是 while (left + 1 < right)
,这说明在退出循环的时候,一定有 left + 1 == right
成立,也就是退出循环以后,区间有 2 个元素,即 [left, right]
;
2、优缺点
- 优点:不用理解模板 2 在分支出现
left = mid
的时候中间位置上/下取整的行为; - 缺点:
while (left + 1 < right)
写法相对于while (left <= right)
和while (left < right)
来说并不自然;由于退出循环以后,区间一定有两个元素,需要思考哪一个元素才是需要找的,即「后处理」一定要做,有些时候还会有先考虑left
还是right
的区别。
3、代码实现
public class BinarySearch {
public static void main(String[] args) {
int[] nums = { 0, 1, 2, 3, 4, 5, 6, 7 };
int target = 2;
System.out.print("数组 nums:");
ArrayUtil.printArray(nums);
System.out.println("目标值 target:" + target);
System.out.println("模板 3 下标:" + binarySearch3(nums, target));
}
/**
* 二分查找法<br>
* <li>模板 3:while (left + 1 < right)</li><br>
* @param nums 待查找数组
* @param target 待查找目标值
* @return 目标值在数组中的下标<br>
* 未查找到就返回 -1
*/
public static int binarySearch3(int[] nums, int target) {
// 特殊用例判断
int len = nums.length;
if (len == 0) {
return -1;
}
// 在 [left, right] 区间里查找 target
int left = 0;
int right = len - 1;
while (left + 1 < right) {
// 选择中间位置时下取整
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid;
} else {
right = mid;
}
}
if (nums[left] == target) {
return left;
}
if (nums[right] == target) {
return right;
}
return -1;
}
}
运行结果:
数组 nums:{ 0, 1, 2, 3, 4, 5, 6, 7 }
目标值 target:2
模板 3 下标:2