LEETCODE / 704

二分查找

从左右边界出发,观察搜索区间如何减半,理解闭区间二分查找的每一步。

开始演示阅读思路原题 ↗

题意概述

在一个严格递增的整数数组中寻找目标值。存在时返回它的下标,不存在时返回 -1。这道题的关键不是逐个检查,而是利用有序性,每次排除一半候选元素。

输入输出示例

一次成功的查找
输入:nums = [-1, 0, 3, 5, 9, 12], target = 9
输出:4
解释:nums[4] 等于 9,下标从 0 开始。

同一个数组中,如果目标改为 2,最终搜索区间会变空,返回 -1

解题思路

维护一个包含左右端点的区间 [left, right]。只要目标存在,它就一定在这个区间里。

  1. 取中点 mid = left + Math.floor((right - left) / 2)
  2. 中点值等于目标,返回 mid
  3. 中点值小于目标,排除中点及其左侧,令 left = mid + 1
  4. 中点值大于目标,排除中点及其右侧,令 right = mid - 1
  5. left > right 时,已经没有候选元素,返回 -1

交互演示

先用「下一步」观察指针变化,再试着播放。把目标改成 2,看看查找失败时的边界。也可以输入单元素数组 9,检查三个指针重合的情况。

二分查找 · 观察区间如何缩小交互演示 · 可修改输入

1–24 个严格递增整数,用逗号分隔;数值范围 -9999~9999。修改后点击「应用输入」。

目标
9
区间
[0, 5]
left
0
right
5
mid
  1. 0-1候选left
  2. 10候选
  3. 23候选
  4. 35候选
  5. 49候选
  6. 512候选right
候选区间正在检查找到答案 ✓上方为下标;数组较长时可左右滚动。
这一步

目标值是 9。从完整区间 [0, 5] 开始,左右边界都包含在搜索范围内。

步骤 1 / 5

初始化闭区间

请启用演示,或应用修改后的输入。 聚焦演示区域后:空格播放 / 暂停,← → 步进。

执行代码 TypeScript · 高亮行随步骤变化
 export function binarySearch(nums: readonly number[], target: number): number {  let left = 0;  let right = nums.length - 1;     while (left <= right) {     const mid = left + Math.floor((right - left) / 2);     if (nums[mid] === target) {       return mid;     }     if (nums[mid]! < target) {       left = mid + 1;     } else {       right = mid - 1;     }   }     return -1; }
查看完整文字步骤(5 步)
  1. 当前步骤: 目标值是 9。从完整区间 [0, 5] 开始,左右边界都包含在搜索范围内。
  2. 检查中点 mid = 2,nums[2] = 3,与目标 9 比较。
  3. 3 < 9,中点及左侧都可排除。left 移到 3,剩余区间 [3, 5]。
  4. 检查中点 mid = 4,nums[4] = 9,与目标 9 比较。
  5. 找到目标 9,返回下标 4。

离开视口或切到后台时自动暂停;返回后可手动继续。

当前为静态初始状态。启用 JavaScript 后可操作演示,也可继续阅读下方解题代码。

代码实现

下面的函数可以独立使用。演示中的执行面板与此处导入同一个 TypeScript 源文件,高亮的是当前步骤对应的实际语句。

binary-search.ts
export function binarySearch(nums: readonly number[], target: number): number {
let left = 0;
let right = nums.length - 1;
while (left <= right) {
const mid = left + Math.floor((right - left) / 2);
if (nums[mid] === target) {
return mid;
}
if (nums[mid]! < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
为什么不能写成 left = mid?

当区间只剩两个元素时,向下取整的中点可能恰好等于 left。如果中点小于目标,却仍让 left = mid,区间就不会缩小,下一轮会重复相同的比较。使用 mid + 1 可以排除已经检查过的元素。

为什么正确

循环开始时,若目标存在,它一定在闭区间 [left, right] 内。比较中点后,由严格递增性可知:目标更大时,中点及左侧都不可能;目标更小时,中点及右侧都不可能。因此每次排除的部分不会包含答案,剩下的区间继续满足这个条件。

每轮都排除中点,区间严格缩小。最终要么找到目标,要么区间为空而返回 -1;不会遗漏答案,也不会无限循环。

复杂度

  • 时间复杂度:O(log n)。每轮将候选区间缩小到大约一半。
  • 额外空间复杂度:O(1)。算法本身只维护左右边界和中点。

边界情况

  • 单元素数组:目标相等返回 0,不相等返回 -1
  • 目标在数组首尾:两个端点都包含在搜索区间内。
  • 目标不存在:可能落在两个元素之间,也可能小于最小值或大于最大值。
  • 输入必须严格递增且没有重复值。演示会报告无序、重复、小数或超出范围的输入,不会自动排序或修改它们。

教学函数也能处理空数组并返回 -1;本题演示按照非空数组的输入约束,限制为 1–24 个元素。

易错点

闭区间需要 left <= right,否则可能漏掉最后一个候选。更新为 mid + 1mid - 1 才能排除已经检查过的中点;不要将闭区间的条件与半开区间的写法混用。

自测与相关题目

预测:数组 [1, 3] 查找 3,第一次比较后 left 是多少?

中点下标为 0,值为 1,小于目标。因此 left 变为 1,下一轮只检查下标 1。若仍写 left = mid,区间不会缩小。

继续阅读 无重复字符的最长子串,观察另一种只向前移动边界的算法。

← 继续阅读其他题目