二分查找全解

本节目标

// 运行环境: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

名词解释

课后练习

  1. binarySearch 为什么用 lo <= hi 而 lowerBound 用 lo < hi?
    • 答案:前者找"确切值",区间空(lo>hi)即不存在返回 -1,故含等号;后者找"边界位置",当 lo==hi 时即为答案,故不取等号、最后返回 lo。
  2. 旋转数组 findMin 里 nums[mid] < nums[hi] 为什么 hi = mid 而不是 mid-1?
    • 答案:此时 mid 可能正好是最左的最小值(它 ≤ 右段所有值),不能排除,必须保留 mid,故 hi=mid;若写 mid-1 会漏掉它。

总结

二分查找是"思路极简、写对极难"的典型。它真正的难点不在"每次砍一半",而在于边界和不变量的设计——lo/hi 是闭区间还是半开?mid 该不该被保留?一旦想清楚"循环不变量"(每轮循环后答案一定还在 [lo,hi] 里),三个模板就能统一推导出来。我强烈建议你把"找等于 / 找左界 / 找右界"三套模板背熟并理解其差异:前者用 lo<=hi 找确切值,后两者用 lo<hi 找插入边界。更进阶的是"二分答案"——它把二分从"查数据"升维到"猜答案再验证",能解平方根、分配问题、最小最大值等一大类题。二分的最高心法就一句:只要"答案空间单调且能判定",就可以二分,不必要求输入数组本身有序。