一、算法概述
二分查找,又称折半查找,是一种针对有序集合的查找算法。其核心逻辑是通过不断将查找区间折半,缩小查找范围,最终定位到目标元素(若存在)。该算法的前提条件是查找的数据集必须是有序的(通常为升序或降序排列),且数据元素需支持比较操作;同时,数据集需采用随机访问方式存储(如数组),以便能够快速定位区间中间位置的元素,这也是二分查找无法直接应用于链表等链式存储结构的核心原因。
二、核心思想与原理
二分查找的核心思想是"分而治之",即将一个大的查找问题拆解为多个小的子问题,通过逐步缩小查找范围,降低问题的求解难度。具体原理可分为以下三个步骤:
1. 初始化查找区间
首先定义两个指针(或索引),分别指向有序数据集的起始位置(通常记为left)和终止位置(通常记为right),此时查找区间为[left, right],覆盖整个数据集。
2. 折半查找与范围缩小
计算查找区间的中间位置(记为mid),公式为$$mid = left + (right - left) / 2$$(采用该公式而非$$mid = (left + right) / 2$$,可避免left与right数值过大时出现的溢出问题)。然后将中间位置的元素(nums[mid])与目标元素(target)进行比较,根据比较结果分为三种情况:
-
若nums[mid] == target:查找成功,返回mid索引,即目标元素在数据集中的位置;
-
若nums[mid] < target:说明目标元素在中间位置的右侧(因数据集有序),此时将left更新为mid + 1,缩小查找区间至[mid + 1, right];
-
若nums[mid] > target:说明目标元素在中间位置的左侧,此时将right更新为mid - 1,缩小查找区间至[left, mid - 1]。
3. 终止条件
重复步骤2,不断折半查找并缩小区间,直到出现以下两种情况之一:
-
找到目标元素,返回其索引;
-
查找区间缩小至left > right,说明目标元素不在数据集中,返回-1(或其他约定的标识),表示查找失败。
三、算法实现(以Java为例)
二分查找有两种常见的实现方式:迭代实现和递归实现。其中,迭代实现因无需递归栈,空间复杂度更低,且避免了递归深度过大导致的栈溢出问题,在实际开发中应用更为广泛。以下分别给出两种实现的代码示例,并附带详细注释。
1. 迭代实现(推荐)
java
/**
* 二分查找迭代实现
* @param nums 有序数组(升序)
* @param target 目标查找元素
* @return 目标元素索引,未找到返回-1
*/
public static int binarySearchIterative(int[] nums, int target) {
// 边界校验:数组为空或长度为0,直接返回-1
if (nums == null || nums.length == 0) {
return -1;
}
int left = 0; // 左指针,指向数组起始位置
int right = nums.length - 1; // 右指针,指向数组终止位置
// 查找区间为[left, right],当left > right时终止
while (left <= right) {
// 计算中间索引,避免left + right溢出
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid; // 找到目标元素,返回索引
} else if (nums[mid] < target) {
left = mid + 1; // 目标在右侧,缩小左边界
} else {
right = mid - 1; // 目标在左侧,缩小右边界
}
}
// 循环结束仍未找到,返回-1
return -1;
}
2. 递归实现
java
/**
* 二分查找递归实现
* @param nums 有序数组(升序)
* @param target 目标查找元素
* @param left 左边界索引
* @param right 右边界索引
* @return 目标元素索引,未找到返回-1
*/
public static int binarySearchRecursive(int[] nums, int target, int left, int right) {
// 边界校验:数组为空或长度为0,直接返回-1
if (nums == null || nums.length == 0) {
return -1;
}
// 终止条件:查找区间无效,未找到目标
if (left > right) {
return -1;
}
// 计算中间索引
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid; // 找到目标,返回索引
} else if (nums[mid] < target) {
// 递归查找右半区间
return binarySearchRecursive(nums, target, mid + 1, right);
} else {
// 递归查找左半区间
return binarySearchRecursive(nums, target, left, mid - 1);
}
}
// 递归调用入口(简化用户调用)
public static int binarySearchRecursive(int[] nums, int target) {
return binarySearchRecursive(nums, target, 0, nums.length - 1);
}
四、关键细节与边界处理
1. 查找区间的定义(左闭右闭 vs 左闭右开)
本文上述实现采用的是"左闭右闭"区间([left, right]),即区间包含左右两个边界的元素。这种情况下,循环终止条件为left > right,且更新left和right时需分别设为mid + 1和mid - 1(因为mid位置已被排查,无需再纳入下一轮区间)。
另一种常见的区间定义是"左闭右开"([left, right)),即左边界包含,右边界不包含。此时循环终止条件为left == right,更新left时设为mid + 1,更新right时设为mid(因right本身不包含在区间内,无需减1)。两种区间定义均可实现二分查找,核心是保持区间定义的一致性,避免混淆。
2. 中间索引的溢出问题
若直接使用mid=(left+right)/2计算中间索引,当left和right均为较大整数(如Java中int类型的最大值)时,left + right会超出int类型的取值范围,导致数值溢出,进而计算出错误的mid值。采用mid=left+(right−left)/2,可通过等价变换避免溢出,其本质与前者一致,但安全性更高。
3. 重复元素的处理
上述实现适用于无重复元素的有序数组,若数组中存在重复元素,二分查找只能找到其中一个目标元素的索引(不一定是第一个或最后一个)。若需求为找到第一个等于target的元素、最后一个等于target的元素,需对算法进行微调,核心是在找到nums[mid] == target时,不立即返回,而是继续缩小区间,确认边界位置。
4. 边界值校验
实现算法时,需优先进行边界校验,如数组为空、数组长度为0、target小于数组最小值或大于数组最大值等情况,可直接返回-1,避免无效循环,提升算法效率。