二分查找全解
本节目标
- 掌握三个二分查找模板(找等于 / 找左边界 / 找右边界)
- 理解"旋转数组中的二分"与"二分答案"
- 能写出不出错的 mid 与边界
// 运行环境:Node.js 14+
// 保存为 dsa-l19.js,执行:node dsa-l19.js
// 模板一:在有序数组找 target(等于即返回)
function binarySearch(a, target) {
let lo = 0, hi = a.length - 1
while (lo <= hi) {
const mid = (lo + hi) >> 1
if (a[mid] === target) return mid
if (a[mid] < target) lo = mid + 1
else hi = mid - 1
}
return -1
}
// 模板二:找 target 的"最左"位置(可不存在,返回插入点)
function lowerBound(a, target) {
let lo = 0, hi = a.length
while (lo < hi) {
const mid = (lo + hi) >> 1
if (a[mid] < target) lo = mid + 1
else hi = mid
}
return lo
}
// 模板三:找 target 的"最右"位置
function upperBound(a, target) {
let lo = 0, hi = a.length
while (lo < hi) {
const mid = (lo + hi) >> 1
if (a[mid] <= target) lo = mid + 1
else hi = mid
}
return lo
}
// 旋转数组中的最小值(含重复):[3,4,5,1,2] -> 1
function findMin(nums) {
let lo = 0, hi = nums.length - 1
while (lo < hi) {
const mid = (lo + hi) >> 1
if (nums[mid] > nums[hi]) lo = mid + 1 // 最小值在右半
else if (nums[mid] < nums[hi]) hi = mid // 在左半(含 mid)
else hi-- // 相等时收缩右界去重
}
return nums[lo]
}
// 二分答案:x 的平方根(向下取整)
function mySqrt(x) {
let lo = 0, hi = x
while (lo <= hi) {
const mid = (lo + hi) >> 1
if (mid * mid <= x) lo = mid + 1
else hi = mid - 1
}
return hi
}
console.log(binarySearch([1,3,5,7], 5)) // 2
console.log(lowerBound([1,2,2,2,3], 2)) // 1
console.log(upperBound([1,2,2,2,3], 2)) // 4
console.log(findMin([3,4,5,1,2])) // 1
console.log(mySqrt(8)) // 2名词解释
- 二分查找(Binary Search):在"有序/单调"区间每次取中点砍掉一半,O(log n) 定位。前提是"答案空间有序且可判定"。
- 下界 / 上界(lowerBound / upperBound):
lowerBound返回"第一个 ≥ target"的位置,upperBound返回"第一个 > target"的位置;二者之差即 target 的出现次数。 - 二分答案(Binary Search on Answer):不直接二分数据,而是二分"答案的取值范围",用判定函数验证可行性。适合"求最大/最小满足某条件的值"。
课后练习
binarySearch为什么用lo <= hi而lowerBound用lo < hi?- 答案:前者找"确切值",区间空(lo>hi)即不存在返回 -1,故含等号;后者找"边界位置",当 lo==hi 时即为答案,故不取等号、最后返回 lo。
- 旋转数组
findMin里nums[mid] < nums[hi]为什么hi = mid而不是mid-1?- 答案:此时
mid可能正好是最左的最小值(它 ≤ 右段所有值),不能排除,必须保留mid,故hi=mid;若写mid-1会漏掉它。
- 答案:此时
总结
二分查找是"思路极简、写对极难"的典型。它真正的难点不在"每次砍一半",而在于边界和不变量的设计——lo/hi 是闭区间还是半开?mid 该不该被保留?一旦想清楚"循环不变量"(每轮循环后答案一定还在 [lo,hi] 里),三个模板就能统一推导出来。我强烈建议你把"找等于 / 找左界 / 找右界"三套模板背熟并理解其差异:前者用 lo<=hi 找确切值,后两者用 lo<hi 找插入边界。更进阶的是"二分答案"——它把二分从"查数据"升维到"猜答案再验证",能解平方根、分配问题、最小最大值等一大类题。二分的最高心法就一句:只要"答案空间单调且能判定",就可以二分,不必要求输入数组本身有序。