Personal Field Archive / Received 2026.09.09
二分查找|LeetCode 第一轮复盘
从搜索目标、区间语义与循环不变量出发,复盘二分查找中最容易失控的边界推理。
章节状态:第一轮已完成 7/7。标准二分与基础边界题已经稳定;需要延迟重做的重点题为 69、34、153、162。
一、题目清单
| 顺序 | LeetCode | 难度 | 题目 | 核心训练 | 第一轮情况 |
|---|---|---|---|---|---|
| 1 | 704 | 简单 | Binary Search | 标准二分、精确目标 | 一遍通过 |
| 2 | 35 | 简单 | Search Insert Position | 插入位置、返回边界 | 一遍通过 |
| 3 | 69 | 简单 | Sqrt(x) | 答案空间二分、边界推进 | 重点复盘 |
| 4 | 367 | 简单 | Valid Perfect Square | 整数边界、精确判定 | 一遍通过 |
| 5 | 34 | 中等 | Find First and Last Position | 左右边界二分 | 重点复盘 |
| 6 | 153 | 中等 | Find Minimum in Rotated Sorted Array | 旋转结构、全局排除证明 | 重点复盘 |
| 7 | 162 | 中等 | Find Peak Element | 局部趋势、存在性证明 | 重点复盘 |
二、当前掌握情况
标准二分的代码框架、基本边界处理以及对有序搜索空间的识别已经建立。704、35、367 等题能够一遍通过,说明目前的主要问题不是“不会写二分”,而是面对变体时,能否严格证明某个判断足以排除一半区间。
现阶段最需要巩固的迁移类型是:
- 答案空间二分: 69 Sqrt(x)
- 边界二分: 34 Search Range
- 依据数组结构排除区间: 153 Find Minimum
- 依据答案存在性排除区间: 162 Find Peak Element
这些题中出现的问题集中在同一根源:能够提出合理的局部判断,但需要进一步证明该判断是否足以支持全局区间排除。
三、二分查找的核心原则
1. 二分的本质是安全地排除一半
二分不只适用于“整个数组有序”的情况。更准确的适用条件是:搜索空间中存在某种单调结构、分界结构或存在性结论,使我们能够根据一次判断证明某一半不包含所需答案。
写下判断条件后,应立即追问:
这个条件能否证明被排除的整个区间都不包含答案?
153 中,nums[mid] < nums[mid + 1] 只是局部递增,不能确定唯一的全局最小值位于哪一侧;162 中,同样的局部上升却能保证右侧至少存在一个峰值。判断式是否充分,取决于题目要求的是唯一特定答案,还是任意一个满足条件的答案。
2. 先定义搜索目标
二分题常见目标包括:
- 精确找到
target; - 找到第一个满足条件的位置;
- 找到最后一个满足条件的位置;
- 找到最大的合法答案;
- 保留任意一个可接受答案。
目标不同,判断条件、边界更新及最终返回值都会变化。不能只根据题目出现“有序数组”就直接套用标准模板。
3. 先定义区间含义,再选择循环条件
| 区间含义 | 循环条件 | 典型更新 | 终止状态 |
|---|---|---|---|
[left, right] 是尚未检查的区域 |
left <= right |
left = mid + 1 或 right = mid - 1 |
left > right,区域为空 |
[left, right] 始终包含答案 |
left < right |
一侧排除 mid,另一侧保留 mid |
left == right,只剩答案 |
标准 target 搜索和 Search Range 的边界搜索通常属于第一种模型;153、162 属于第二种模型。
不要简化成“查 target 用 <=,其他题用 <”。真正应该判断的是:
检查
mid后,是否一定排除mid?
4. 每次更新必须保证区间严格缩小
普通偏左中点:
mid = (left + right) // 2
当区间为 [2, 3] 时,mid == 2。因此:
left = mid
不会缩小区间,可能导致死循环。若必须保留 mid 并更新左边界,应使用偏右中点:
mid = (left + right + 1) // 2
实用对应关系:
right = mid → 通常使用普通偏左中点
left = mid → 通常使用偏右中点
每次写完边界更新,都用 [2, 3] 和 [2, 2] 代入,检查区间是否继续推进以及访问是否越界。
5. 区分索引、索引处的值和题目要求的返回值
mid 是索引
nums[mid] 是该索引对应的值
left/right 通常是搜索边界索引
target 是目标值
34 中必须比较 nums[mid] 与 target,不能比较 mid 与 target。返回内容同样要服从题意:153 返回最小元素的值,所以返回 nums[left];162 返回峰值索引,所以返回 left。
6. 循环结束后必须解释边界语义
不要把 return left 或 return right 当成固定结尾。必须从循环不变量和终止状态推导答案:
left > right:未检查区域为空,答案可能由提前返回、记录变量或分界位置表示;left == right:如果循环始终保证区间包含答案,那么唯一剩余位置就是答案;- 边界搜索结束:
left、right往往分别停在真假区域的分界两侧。
四、重点题复盘
69. Sqrt(x)
题目目标
给定非负整数 x,返回算术平方根向下取整后的整数。可以把整数答案作为有序搜索空间,并定义:
n² <= x → 合法
n² > x → 不合法
本题要求寻找最大的合法整数,即“最后一个合法值”。
原始问题
原思路使用:
while left <= right:
mid = (left + right) // 2
if mid * mid < x:
left = mid
将 mid 保留为候选答案的想法合理,但普通中点可能等于 left。例如 [2, 3] 中 mid == 2,执行 left = mid 后区间不变;[2, 2] 中更会无限重复。
另外,right = x // 2 会遗漏 x == 1。
与原始直觉一致的写法
class Solution:
def mySqrt(self, x: int) -> int:
left, right = 0, x
while left < right:
mid = (left + right + 1) // 2
if mid * mid <= x:
left = mid
else:
right = mid - 1
return left
循环不变量:[left, right] 始终包含最终答案。使用偏右中点后,left = mid 仍然能够保证区间缩小。
记录候选后排除 mid 的写法
class Solution:
def mySqrt(self, x: int) -> int:
left, right = 0, x
answer = 0
while left <= right:
mid = (left + right) // 2
if mid * mid <= x:
answer = mid
left = mid + 1
else:
right = mid - 1
return answer
left = mid + 1 不表示 mid 不是答案;answer 已经保存了它,算法只是继续寻找更大的合法值。
34. Find First and Last Position of Element in Sorted Array
正确建模
分别寻找:
左端点 = 第一个 nums[i] >= target 的位置
右端点 = 第一个 nums[i] > target 的位置 - 1
第一轮出现的问题
- 比较了索引
mid与target,而不是nums[mid]与target。 first_greater返回第一个严格大于target的位置,最终右端点必须减一。- 在确认索引合法之前访问
nums[left_bound],空数组或target大于全部元素时会触发IndexError。 - 使用整数结果覆盖内部函数名,降低可读性;
grater还应拼写为greater。
正确写法
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
def first_equal_or_greater(target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return left
def first_greater(target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = (left + right) // 2
if nums[mid] <= target:
left = mid + 1
else:
right = mid - 1
return left
left_bound = first_equal_or_greater(target)
if left_bound >= len(nums) or nums[left_bound] != target:
return [-1, -1]
right_bound = first_greater(target) - 1
return [left_bound, right_bound]
对 [5, 7, 7, 8, 8, 10] 和 target = 8,第一个函数返回 3,第二个函数返回 5,因此结果为 [3, 5 - 1] = [3, 4]。
153. Find Minimum in Rotated Sorted Array
旋转数组结构
无重复元素的旋转升序数组可以看作:
较大值升序段 + 较小值升序段
[4, 5, 6, 7] + [0, 1, 2]
第一轮的根本问题
nums[mid] < nums[mid + 1] 这个比较结果没有错;错误的是从局部递增推出“最小值一定在左侧”。mid 与 mid + 1 可能共同位于任意一个升序段,所以局部关系不足以判断旋转断点位于哪边。
另外还存在两个实现问题:mid + 1 可能越界;return left 返回索引,而题目要求返回最小元素的值。
为什么比较 nums[mid] 与 nums[right]
nums[mid] > nums[right]
→ mid 位于较大值段,断点在 mid 右侧
→ 最小值在 [mid + 1, right]
nums[mid] < nums[right]
→ [mid, right] 有序,其中最小的是 nums[mid]
→ 全局最小值在 [left, mid]
正确写法
class Solution:
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] > nums[right]:
left = mid + 1
else:
right = mid
return nums[left]
循环不变量:最小值的索引始终存在于 [left, right]。另一个分支必须写 right = mid,因为 mid 可能就是最小值。
162. Find Peak Element
为什么 nums[mid + 1] 不越界
循环使用:
while left < right:
普通中点向下取整,因此进入循环时必然有:
mid < right
mid + 1 <= right <= len(nums) - 1
所以 nums[mid + 1] 一定合法。left == right 时循环已经停止,不会再次访问右侧元素。
为什么局部趋势足够
如果:
nums[mid] < nums[mid + 1]
当前位置处于上坡。向右继续,要么在某处由上升转为下降并形成峰值,要么一直上升到最右端,而最右端相对于数组外侧的负无穷也是峰值。因此右侧至少存在一个峰值,可以执行 left = mid + 1。
否则当前位置处于下坡,左侧(包括 mid)至少存在一个峰值;mid 本身可能是峰值,所以执行 right = mid。
正确写法
class Solution:
def findPeakElement(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
while left < right:
mid = (left + right) // 2
if nums[mid] < nums[mid + 1]:
left = mid + 1
else:
right = mid
return left
153 与 162 的关键差异:153 必须保留唯一的全局最小值,局部趋势不足以定位它;162 允许返回任意峰值,只要局部趋势能证明某一侧至少存在一个峰值,就可以放弃另一侧。
五、写二分代码前的检查清单
- 搜索空间是数组索引、数值答案,还是某个可行范围?
- 要找的是精确目标、第一个合法值、最后一个合法值,还是任意满足条件的位置?
- 判断条件是否具有足够的全局信息,能证明被排除的一半不含所需答案?
[left, right]表示尚未检查区域,还是一定包含答案的候选区间?- 每个分支中,
mid是被排除,还是可能为答案而必须保留? - 把
[2, 3]和[2, 2]代入后,边界是否严格推进?mid + 1是否仍在界内? - 循环结束时是
left > right还是left == right?这如何推出最终返回值? - 返回的是索引还是元素值?
- 空数组、单元素、目标不存在、答案位于两端时是否安全?
六、复杂度
上述题目每轮都排除约一半搜索空间,典型时间复杂度为 O(log n);若搜索的是数值答案区间 [0, x],可写为 O(log x)。只使用常数个边界与中点变量时,额外空间复杂度为 O(1)。
七、延迟重做计划
在完成第一轮后的 3~7 天内,从空白重做:
69、34、153、162
每道题除通过测试外,还应能回答:
- 搜索空间是什么?
- 循环不变量是什么?
- 为什么判断条件足以排除这一半?
- 为什么不会死循环或越界?
- 循环结束后为什么返回该索引或该值?
如果四题均能独立完成并解释上述问题,可以将本章标记为“稳定掌握”;如果只能重现代码但无法解释区间含义,应继续保留在复习队列中。