“在有序的世界里,二分查找是最快的猎手。”
在算法面试和工程实践中,二分查找(Binary Search) 是最基础也最重要的搜索算法之一。它以 O(log n) 的时间复杂度,在有序数据中高效定位目标,远胜线性查找的 O(n)。但看似简单的代码背后,却藏着无数边界陷阱。本文将带你彻底掌握二分查找的核心思想、多种写法、常见变种及避坑技巧。
一、什么是二分查找?
二分查找是一种在有序数组中查找特定元素的高效算法。其基本思想是:
每次将搜索区间一分为二,通过比较中间元素与目标值的大小,决定继续在左半部分还是右半部分查找,直到找到目标或区间为空。
🌰 举个例子:
在升序数组 [1, 3, 5, 7, 9, 11] 中查找 7:
| 步骤 | left | right | mid | nums[mid] | 操作 |
|---|---|---|---|---|---|
| 1 | 0 | 5 | 2 | 5 | 5 < 7 → 查右半 |
| 2 | 3 | 5 | 4 | 9 | 9 > 7 → 查左半 |
| 3 | 3 | 3 | 3 | 7 | 找到! |
仅用 3 次比较就完成了查找(而线性查找最多需 6 次)。
二、使用前提
✅ 二分查找必须满足以下条件:
- 数据结构支持随机访问(如数组,不能是链表);
- 元素必须有序(升序或降序,默认讨论升序);
- 若存在重复元素,需明确查找目标(任意位置?最左?最右?)。
❌ 在无序数组上使用二分查找,结果不可预测!
三、基础实现:两种主流写法
方法一:闭区间写法(推荐 ✅)
function binarySearch(nums, target) {
let left = 0, right = nums.length - 1; // [left, right]
while (left <= right) {
const mid = left + Math.floor((right - left) / 2); // 防溢出
if (nums[mid] === target) {
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1; // 未找到
}
- 循环条件:
left <= right(区间非空) - 优点:直观、易理解,适用于大多数场景
方法二:开区间写法(更安全,适合变种)
function lowerBound(nums, target) {
let left = -1, right = nums.length; // (left, right)
while (left + 1 < right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] >= target) {
right = mid;
} else {
left = mid;
}
}
return right; // 第一个 >= target 的下标
}
-
不变量:
nums[left] < targetnums[right] >= target
-
优势:天然避免死循环,特别适合处理边界问题(如找左/右边界)
四、常见变种(高频面试题)
普通二分只能找“任意一个目标”,但在实际问题中,我们常需要:
| 变种 | 功能 | 应用场景 |
|---|---|---|
| 左边界(Lower Bound) | 返回第一个 ≥ target 的下标 | 找第一个出现的位置 |
| 右边界(Upper Bound) | 返回第一个 > target 的下标 | 找最后一个出现的位置 |
| 搜索插入位置 | 若不存在,返回应插入的位置 | LeetCode 35 |
| 二分答案 | 在答案空间中二分,验证可行性 | 求最小最大值、最大最小值等 |
📌 实战:查找元素的第一个和最后一个位置(LeetCode 34)
var searchRange = function(nums, target) {
const start = lowerBound(nums, target);
if (start === nums.length || nums[start] !== target) {
return [-1, -1];
}
const end = lowerBound(nums, target + 1) - 1;
return [start, end];
};
利用两次
lowerBound,优雅解决重复元素问题!
五、常见误区与避坑指南
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 死循环 | 更新边界时区间未缩小(如 left = mid) | 确保每次更新后区间严格缩小 |
| 整数溢出 | (left + right) / 2 在大数时溢出 | 改用 left + (right - left) / 2 |
| 漏查元素 | 循环条件写成 left < right | 使用 left <= right 或明确不变量 |
| 忽略重复元素 | 普通二分返回任意匹配位置 | 使用左/右边界变种 |
💡 口诀:
“左加一,右减一;开闭区间要统一;不同元素看 Map,边界条件多测试。 ”
六、为什么不用三分、四分查找?
虽然直觉上“多分”可能更快,但数学证明表明:
二分查找是最优的。
对于 n 个元素,k 分查找的时间复杂度为 O(k·logₖn),当 k=2 时取得最小值。
七、推荐练习题(LeetCode)
| 题号 | 题目 | 类型 |
|---|---|---|
| 704 | 二分查找 | 基础 |
| 35 | 搜索插入位置 | 边界处理 |
| 34 | 查找第一个和最后一个位置 | 左右边界 |
| 69 | x 的平方根 | 二分答案 |
| 875 | 爱吃香蕉的珂珂 | 二分答案应用 |
八、总结
- 二分查找是有序结构中的利器,时间复杂度 O(log n)。
- 掌握闭区间 vs 开区间两种写法,灵活应对不同场景。
- 面对重复元素、边界问题时,优先考虑左/右边界变种。
- 细节决定成败:防溢出、循环条件、更新逻辑缺一不可。
✨ 建议:不要死记模板,而是理解“二段性”——只要一个问题能被划分为“满足条件”和“不满足条件”两部分,就可以尝试二分!