二分查找算法
二分查找算法(Binary Search Algorithm)是一种用于在有序数组或列表中查找特定元素的算法。它的工作原理是通过比较目标值与数组中间元素的大小关系,从而将查找范围缩小一半,并在剩余部分继续查找,直到找到目标值或确定它不存在。
二分查找算法主要用于在有序集合中查找特定元素,其时间复杂度为 O(log n),相比线性查找的 O(n) 时间复杂度更低,因此适用于大型数据集。它可以在数据量很大时提供较高的搜索效率。
二分查找算法主要实现场景包括但不限于:
查找有序数组或列表中的指定元素:最典型的用法是在有序数组或列表中查找特定元素的位置。
查找边界问题:可以用于解决一些边界问题,如查找某个值的第一个出现位置或最后一个出现位置。
搜索范围的缩小:在一些需要搜索某个特定值的区间范围内的场景中,可以通过二分查找来加速搜索过程。
二分查找算法是一种高效的查找方法,适用于已知数据是有序的场景。它通常在需要快速查找有序集合中的元素时发挥作用,能够大大提高搜索效率和性能。
二分查找算法实现
1、非递归方法实现
public class BinarySearchTest {
public static void main(String[] args) {
// 创建有序数组
int[] arr = {1, 3, 5, 7, 9, 11, 13, 15, 17, 19};
// 选择要查找的元素
int searchElem = 17;
// 二分查找
int result = binarySearch(arr, searchElem);
if (result != -1) {
System.out.println("找到元素 " + searchElem + " 在索引 " + result);
} else {
System.out.println("未找到元素 " + searchElem);
}
}
/**
* 非递归版本的二分查找
*/
private static int binarySearch(int[] arr, int x) {
int left = 0;
int right = arr.length - 1;
// 判断数组是否为空
while (left <= right) {
// 获取中位数
int mid = left + (right - left) / 2;
if (arr[mid] < x) {
left = mid + 1;
} else if (arr[mid] > x) {
right = mid - 1;
} else {
// 找到元素,返回索引
return mid;
}
}
// 元素不在数组中
return -1;
}
}
运行结果:
2、递归方法实现
private static int binarySearchR(int[] arr, int target, int left, int right) {
// 边界条件
if (left > right) {
return -1;
}
// 获取中位数
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
}
if (arr[mid] < target) {
// 在右半部分继续查找
return binarySearchR(arr, target, mid + 1, right);
}else {
// 在左半部分继续查找
return binarySearchR(arr, target, left, mid - 1);
}
}
int mid = left + (right - left) / 2 和 int mid = (left + right) / 2 的区别:
int mid = left + (right - left) / 2 和 int mid = (left + right) / 2 这两个表达式都用于计算两个整数 left 和 right 的中间值,但是它们之间存在一个关键的区别,尤其是在处理整数溢出时。
整数溢出:
int mid = (left + right) / 2: 当 left 和 right 都是很大的正整数,并且它们的和超过了 int 类型的最大值时,就会发生整数溢出。溢出的结果会导致计算出的 mid 值不正确。
int mid = left + (right - left) / 2: 这个表达式首先计算 right - left,这个结果通常不会比 left 或 right 本身大,因此不太可能溢出。然后再将结果除以 2,并最后加上 left。这种方法大大降低了整数溢出的风险。
结果:
在没有溢出的情况下,这两个表达式通常会得到相同的结果。
在发生溢出的情况下,int mid = (left + right) / 2 会产生不正确的结果,而 int mid = left + (right - left) / 2 则能够得出正确的中间值(在整数范围内)。
二分查找算法变种
首先,为什么会有二分查找算法的变种?其实二分查找算法的变种是为了更好地适应不同的查找需求和场景。原始的二分查找算法虽然在一般情况下能够高效地解决大部分的查找问题,但某些特定情况下可能无法满足特定的需求,比如数据重复等等场景。
因此,为了应对不同的查找要求,出现了一系列针对不同场景的二分查找算法变种。
二分查找算法常见的算法变种:
1、查找第一个等于给定值的元素:在经典的二分查找算法基础上做修改,使其返回第一个等于给定值的元素的索引。如果找到一个等于目标值的元素后,检查它的前一个元素是不是还等于目标值,如果不等于,那么当前找到的这个值就是第一个等于目标值的元素。
public static int firstOccurrenceBinarySearch(int[] arr, int value) {
int low = 0;
int high = arr.length - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (arr[mid] < value) {
low = mid + 1;
} else if (arr[mid] > value) {
high = mid - 1;
} else {
if (mid == 0 || arr[mid - 1] != value) {
return mid;
}
high = mid - 1;
}
}
return -1;
}
2、查找最后一个等于给定值的元素:同样是在经典的二分查找算法基础上做修改,使其返回最后一个等于给定值的元素的索引。如果找到一个等于目标值的元素后,检查它的后一个元素是不是还等于目标值,如果不等于,那么当前找到的这个值就是最后一个等于目标值的元素。
public static int lastOccurrenceBinarySearch(int[] arr, int value) {
int low = 0;
int high = arr.length - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (arr[mid] < value) {
low = mid + 1;
} else if (arr[mid] > value) {
high = mid - 1;
} else {
if (mid == arr.length - 1 || arr[mid + 1] != value) {
return mid;
}
low = mid + 1;
}
}
return -1;
}
3、查找第一个大于等于给定值的元素:和经典的二分查找算法类似,但是当给定值小于等于中间值时,需要在左半部分继续搜索以寻找第一个大于等于给定值的元素。
public static int firstGreaterOrEqualBinarySearch(int[] arr, int value) {
int low = 0;
int high = arr.length - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (arr[mid] < value) {
low = mid + 1;
} else {
if (mid == 0 || arr[mid - 1] < value) {
return mid;
}
high = mid - 1;
}
}
return -1;
}
4、查找最后一个小于等于给定值的元素:类似于经典的二分查找算法,但当给定值小于中间值时,需要在左半部分继续搜索以找到最后一个小于等于给定值的元素。
public static int lastLessOrEqualBinarySearch(int[] arr, int value) {
int low = 0;
int high = arr.length - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (arr[mid] > value) {
high = mid - 1;
} else {
if (mid == arr.length - 1 || arr[mid + 1] > value) {
return mid;
}
low = mid + 1;
}
}
return -1;
}
5、循环结束条件的变种:可以根据具体情况改变循环结束的条件,比如只要找到一个等于目标值的元素即可结束循环,或者考虑查找范围只有1或2个元素时的特殊情况。
public static int binarySearchWithVariantEndingCondition(int[] arr, int value) {
int low = 0;
int high = arr.length - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (arr[mid] == value) {
return mid; // 找到元素,返回索引
} else if (arr[mid] < value) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1; // 元素不在数组中
}
小结:
结合实际开发情况,在使用二分查找算法时,有几个关键的注意事项需要特别留意:
-
数据必须是有序的:二分查找算法要求操作的集合必须是有序的,否则无法使用这种算法。因此,在应用二分查找算法时,要确保数据是有序的,如果不是,则需要先进行排序操作。
-
边界条件:要仔细处理边界条件,确保算法能够正确地处理搜索区间只有一个元素、没有找到目标值以及找到目标值的重复情况。
-
死循环:如果实现时出现死循环,可能是由于计算中间值的方式不当导致的。要确保循环能够正常终止。
-
数组越界问题:要注意避免数组越界,特别是当 low 或 high 发生变化时,以及计算 mid 值的过程中。
-
迭代和递归选择:可以使用迭代或递归两种不同的方式实现二分查找算法。在选择时需要根据具体情况衡量其优劣。
-
特殊情况处理:在处理边界情况、特殊值和特殊需求时要格外小心,确保算法的鲁棒性和正确性。
-
性能优化:在具体场景下,还可以考虑一些性能优化技巧,如减少比较次数、充分利用有序性质等方法提高算法效率。
总的来说,使用二分查找算法时需要对特定的场景和需求进行慎重考虑,并确保对边界条件进行妥善处理,以保证算法的正确性和稳定性。