算法二分法详解
二分查找是一种高效的查找算法,时间复杂度为 O(logn),但实际应用中容易出现边界条件错误。掌握以下技巧可以帮助你写出更简洁、正确的二分查找代码:
一、明确二分查找的适用场景
- 必须是有序数组(单调递增或递减),且数组中元素可比较。
- 无重复元素或允许重复但能处理边界(如查找重复元素的第一个 / 最后一个位置)。
二、核心框架:两种「区间定义」模式
二分查找的关键是明确搜索区间的定义,并始终遵守该定义(循环不变量)。常见的两种模式:
1. 左闭右闭区间 [left, right]
(推荐新手)
- 初始条件:
left = 0
,right = n - 1
(n
为数组长度)。 - 循环条件:
while (left <= right)
(因为left == right
时,[left, right]
是有效区间)。 - 边界调整:
- 若
nums[mid] < target
:目标在右侧,调整left = mid + 1
(mid
已排除)。 - 若
nums[mid] > target
:目标在左侧,调整right = mid - 1
(mid
已排除)。 - 若
nums[mid] == target
:找到目标,返回mid
(或根据需求处理,如找边界)。
- 若
- 终止条件:
left > right
,此时区间无效,返回-1
(未找到)。
cpp
运行
int binarySearch(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1;while (left <= right) {int mid = left + (right - left) / 2; // 避免 (left+right) 溢出if (nums[mid] == target) {return mid; // 找到目标} else if (nums[mid] < target) {left = mid + 1; // 目标在右侧} else {right = mid - 1; // 目标在左侧}}return -1; // 未找到
}
2. 左闭右开区间 [left, right)
- 初始条件:
left = 0
,right = n
(右侧开区间,不包含right
)。 - 循环条件:
while (left < right)
(left == right
时区间无效)。 - 边界调整:
- 若
nums[mid] < target
:调整left = mid + 1
(同左闭右闭)。 - 若
nums[mid] > target
:调整right = mid
(右侧开区间,mid
本身不包含在新区间)。 - 若
nums[mid] == target
:返回mid
。
- 若
- 终止条件:
left == right
,返回-1
。
cpp
运行
int binarySearch(vector<int>& nums, int target) {int left = 0, right = nums.size();while (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; // 右侧开区间,不包含mid}}return -1;
}
选择建议:新手优先用「左闭右闭」,逻辑更直观;熟悉后可根据场景灵活切换。
三、处理「重复元素」:查找边界
当数组有重复元素时,二分查找可用于定位目标的第一个出现位置或最后一个出现位置。
1. 查找第一个大于等于 target
的位置(左边界)
cpp
运行
int findFirstGe(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1;int res = nums.size(); // 默认目标比所有元素大while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] >= target) {res = mid; // 记录可能的左边界right = mid - 1; // 继续向左查找更小的边界} else {left = mid + 1;}}return res;
}
2. 查找最后一个小于等于 target
的位置(右边界)
cpp
运行
int findLastLe(vector<int>& nums, int target) {int left = 0, right = nums.size() - 1;int res = -1; // 默认目标比所有元素小while (left <= right) {int mid = left + (right - left) / 2;if (nums[mid] <= target) {res = mid; // 记录可能的右边界left = mid + 1; // 继续向右查找更大的边界} else {right = mid - 1;}}return res;
}
四、避免常见错误
计算
mid
时溢出错误写法:mid = (left + right) / 2
(当left
和right
很大时,left + right
可能超过int
范围)。正确写法:mid = left + (right - left) / 2
(等价于前者,且避免溢出)。边界调整错误忘记根据区间定义调整
left
或right
(如左闭右闭区间中,漏写mid + 1
或mid - 1
,导致死循环)。循环条件错误左闭右闭区间用
left < right
会漏掉left == right
的情况;左闭右开区间用left <= right
会导致死循环。未处理「目标不存在」的情况需在循环结束后判断结果是否有效(如查找边界时,
res
是否在数组范围内)。
五、扩展技巧:二分查找的「泛化」
二分查找不仅用于数组,还可用于具有单调性的函数(如求方程的解、最大值最小化问题)。核心是:
- 确定「搜索范围」(
left
和right
的初始值)。 - 定义「判断条件」(
check(mid)
),使得区间可按单调性划分。
例如,求满足 f(x) = 0
的解(f(x)
单调递增):
cpp
运行
double findRoot() {double left = 0.0, right = 100.0; // 假设解在[0, 100]内while (right - left > 1e-6) { // 精度控制double mid = left + (right - left) / 2;if (f(mid) >= 0) { // f(x)递增,解在左侧right = mid;} else {left = mid;}}return left;
}
六、总结
- 明确区间定义:坚持「循环不变量」,左闭右闭或左闭右开二选一。
- 边界调整与循环条件匹配:根据区间定义调整
left
/right
,避免死循环。 - 处理重复元素:通过「记录中间结果 + 收缩区间」查找左 / 右边界。
- 泛化应用:将问题转化为「单调区间内的查找」,灵活定义
check
函数。
掌握这些技巧后,二分查找的各类变体问题(如搜索旋转数组、寻找峰值等)都能迎刃而解。