二分算法(也称为二分法或折半查找)是一种在有序数组中查找特定元素的搜索算法。其基本原理是通过不断缩小查找范围来逼近目标值。以下是二分算法的详细讲解:
基本原理
- 有序性:二分算法要求待搜索的数组必须是有序的(通常是升序或降序)。
- 缩小范围:算法通过比较目标值与数组中间元素的大小,将搜索范围缩小为原范围的一半。
- 循环迭代:重复上述过程,直到找到目标值或确定目标值不存在为止。
实现步骤
- 确定初始范围:根据数组的大小,确定初始的搜索范围(通常是数组的起始和结束索引)。
- 计算中间位置:通过计算范围的中间位置(通常使用
(left + right) / 2
或left + (right - left) / 2
以避免整数溢出),得到中间元素的索引。 - 比较中间元素:将目标值与中间元素进行比较。
- 如果目标值等于中间元素,则查找成功,返回中间元素的索引。
- 如果目标值小于中间元素,则在数组的左半部分继续查找(更新右边界为
mid - 1
)。 - 如果目标值大于中间元素,则在数组的右半部分继续查找(更新左边界为
mid + 1
)。
- 循环迭代:重复步骤2和3,直到找到目标值或搜索范围为空(即
left > right
)。
时间复杂度
二分算法的时间复杂度为O(log n),其中n是数组的长度。这意味着随着数据量的增加,其查找时间并不会呈线性增长,而是呈对数增长。因此,二分算法是一种非常高效的搜索算法,尤其适用于大规模数据的查找。
适用范围
二分算法适用于解决具有“二段性”(单调性)的问题,通常表现为求解满足某一条件的最大值或最小值问题。例如,在有序数组中查找某个特定的值、在范围内查找满足特定条件的数据等。
下面是一个使用Java编写的二分查找算法的详细示例。这个算法假设我们要在一个已排序的整数数组中查找一个特定的整数。
public class BinarySearch {
// 二分查找算法
public static int binarySearch(int[] arr, int target) {
if (arr == null || arr.length == 0) {
// 如果数组为空,返回-1表示未找到
return -1;
}
int left = 0; // 搜索范围的左边界
int right = arr.length - 1; // 搜索范围的右边界
while (left <= right) {
// 计算中间位置,防止溢出
int mid = left + (right - left) / 2;
// 检查目标值是否在中间位置
if (arr[mid] == target) {
// 如果找到,返回中间位置的索引
return mid;
} else if (arr[mid] < target) {
// 如果目标值大于中间值,搜索右半部分
left = mid + 1;
} else {
// 如果目标值小于中间值,搜索左半部分
right = mid - 1;
}
}
// 如果循环结束仍未找到,返回-1表示未找到
return -1;
}
// 测试方法
public static void main(String[] args) {
int[] arr = {2, 3, 4, 10, 40}; // 已排序的数组
int target = 10; // 要查找的目标值
int result = binarySearch(arr, target);
if (result == -1) {
System.out.println("目标值 " + target + " 未在数组中找到");
} else {
System.out.println("目标值 " + target + " 在数组中的索引为 " + result);
}
}
}
重点问题:这里的循环条件为什么是left <= right而不是left < right
- 确保搜索范围有效,防止遗漏检查:
- 当
left
指向数组的起始位置,而right
指向数组的末尾位置时,搜索范围是有效的。 - 如果我们使用
left < right
作为条件,那么在最后一次迭代中,当left
和right
相等时,循环就会结束,但我们还没有检查这个共同的索引位置是否包含目标值。 - 只有在
left
和right
相等时,我们才能确定搜索范围内已经没有更多的元素可以检查,因此可以安全地返回未找到的结果。
- 当
变形来啦!
如果我特意把int right = arr.length - 1; 改为int right = arr.length ; 代码又该如何书写
当将 right
初始化为 arr.length
时,right
本身并不表示一个有效的数组索引(因为有效的索引是从0到arr.length - 1
)。因此,在循环内部,你永远不会直接使用 right
作为索引来访问数组元素。相反,你会使用计算出的中间索引 mid
。
将循环条件更改为 left < right
是为了确保在循环过程中 left
和 right
不会相等,除非它们已经收敛到同一个位置(即目标值应该存在的位置,或者如果目标值不存在,则它们收敛到插入点)。
在标准的二分查找中,当 left == right
时,我们检查 arr[left]
(或 arr[right]
,因为它们此时是相等的)是否等于目标值。但是,在你的特定情况下,因为 right
被初始化为 arr.length
,所以你不能直接检查 arr[right]
。因此,你需要在循环继续执行直到 left < right
,并且在循环结束后单独检查 arr[left]
是否等于目标值(如果循环因 left == right
而结束)。
public static int binarySearch(int[] arr, int target) {
if (arr == null || arr.length == 0) {
return -1;
}
int left = 0;
int right = arr.length; // 注意这里是arr.length,但我们不会直接用它来索引
while (left < right) { // 注意循环条件变为left < right
int mid = left + (right - left) / 2; // 计算中间索引时仍然安全
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid; // 注意这里将right设置为mid,而不是mid - 1
}
}
// 循环结束后,left == right,如果目标值在数组中,它应该在left/right-1的位置
// 但因为我们没有检查left/right-1,所以返回-1表示未找到
return -1;
}
如果有序数组中连续有几个数相同,我要返回最左边的索引,代码又该如何书写
public static int binarySearchLeftmost(int[] arr, int target) {
if (arr == null || arr.length == 0) {
return -1;
}
int left = 0;
int right = arr.length - 1;
int result = -1; // 初始化结果为-1,表示未找到
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] >= target) { // 注意这里使用 >= 而不是 ==
// 如果找到了目标值或者中间值比目标值大(因为是有序数组),则向左搜索
result = mid; // 先保存当前索引,因为我们可能还没找到最左边的
right = mid - 1; // 继续向左搜索
} else {
left = mid + 1; // 向右搜索
}
}
// 如果result没有被更新过,说明没有找到目标值
return result;
}
在这个实现中,我们使用了 >=
来判断中间值是否大于或等于目标值。这是因为当找到目标值时,我们想要继续向左搜索以找到最左边的索引。如果 arr[mid]
等于 target
,我们更新 result
并将 right
设置为 mid - 1
以继续向左搜索。如果 arr[mid]
大于 target
,我们同样向左搜索。如果 arr[mid]
小于 target
,我们则向右搜索。
当循环结束时,result
将包含最左边的目标值的索引(如果存在的话),或者如果没有找到目标值,则 result
将保持为初始值 -1
。