当前:LC704 · 二分查找 · 首次出现于 Day 6 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC704 · Binary Search · 二分

二分查找:每一次排除一半

有序数组里找 target——比较中点,扔掉不含答案的那一半。

维护查找区间 [left, right]。每次取中点 mid = left + (right−left)/2:nums[mid] 等于 target 就命中;小于 target 说明答案在右半边,令 left = mid+1;大于 target 说明在左半边,令 right = mid−1。区间为空(left > right)仍未命中则返回 −1。O(log n)。

时间 O(log n)空间 O(1)结论先行 · 全文约 6 节
导读

给定一个升序数组 nums 和一个目标值 target,返回它在数组中的下标;不存在返回 −1。

示例:nums = [−1,0,3,5,9,12],target = 9 → 4。

二分的关键不是「找中点」这个动作,而是理解它背后的不变量。

为什么能折半

数组升序:如果 nums[mid] < target,那么 mid 及左侧所有元素都小于 target,全部可以排除。

这就是二分的全部:根据中点与 target 的大小关系,确定性地排除一半。

所以每一步区间长度减半,log₂n 步后区间为空或命中。

前提折半依赖单调性:只有升序(或降序),才能从中点推出「哪半边还有答案」。

折半演示

nums = [−1,0,3,5,9,12],target = 9。

left=0, right=5, mid=2。nums[2]=3 < 9,排除左半边,left=3。

left=3, right=5, mid=4。nums[4]=9 == 9,命中,返回 4。

只用两步就找到——每次排除一半的力量。

比较中点,排除不含 target 的半边
-1
0
0
1
3
2
5
3
9
4
12
5
[left=0, right=5]mid=2nums[2]=3 < 9 → 排除左半边

不变量:target 永远只在区间里

关键不变量:如果 target 存在,它一定在 [left, right] 里。

每次更新都严格排除「确定不含 target」的那半边,所以不变量保持。

循环条件 while (left <= right):left == right 时还有一个元素要检查,不能停。

left = mid+1、right = mid−1(而不是 mid)是为了排除 mid 本身——mid 已被比较过,不再属于搜索区间。

未命中:区间收缩到空

target = 2 在上述数组里不存在。

mid=2 时 nums[2]=3 > 2 → right=1。区间 [0,1]。

mid=0 时 nums[0]=−1 < 2 → left=1。区间 [1,1]。

mid=1 时 nums[1]=0 < 2 → left=2。此时 left > right,区间为空,返回 −1。

注意循环结束位置:left 恰好是「第一个大于 target」的下标,这给 LC35(搜索插入位置)留了伏笔。

未命中时区间收缩到空,返回 −1
-1
0
0
1
3
2
5
3
9
4
12
5
[left=0, right=2]mid=1nums[1]=0 < 2 → left=2

七行 Go:闭区间二分

solution.goGo
func search(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
mid := left + (right-left)/2
if nums[mid] == target { return mid }
if nums[mid] < target { left = mid + 1 } else { right = mid - 1 }
}
return -1
}

1闭区间 [left, right]。

2区间非空就继续。

3防止溢出的中点写法。

4命中返回。

5排除不含答案的半边。

总结

比较中点、排除半边、维护不变量——O(log n) 有序查找的标准动作。

  • 单调性是二分的前提。
  • 不变量「target 只在 [left,right] 里」约束了每一步更新。
  • left=mid+1、right=mid−1 排除已比较的 mid。
同族题目
LC35搜索插入位置LC34在排序数组中查找元素首尾位置LC33搜索旋转排序数组