1. 二分查找算法基础与力扣刷题实战指南
第一次在力扣上遇到二分查找类题目时,我盯着那个"简单"的难度标签整整十分钟——明明知道这个算法的基本思想,但面对具体问题时总是陷入死循环或者边界条件错误。后来通过系统性地刷完力扣二分查找专题的53道题目,才发现掌握这个"简单"算法需要突破三个认知层级:理解循环不变量、掌握模板变形、识别隐藏的二分场景。本文将分享我从力扣700+题经验中总结的二分查找实战体系。
二分查找之所以成为算法面试的必考知识点,是因为它在O(log n)时间复杂度下能高效解决搜索问题,这比线性搜索的O(n)快了几个数量级。但真正让初学者头疼的不是算法本身,而是各种边界条件的处理。比如当看到力扣第34题"在排序数组中查找元素的第一个和最后一个位置"时,很多人会疑惑:为什么同样的二分逻辑要写两遍?这其实涉及到查找左右边界的核心差异。
2. 二分查找算法原理深度解析
2.1 循环不变量的关键作用
二分查找的核心是维护循环不变量——在整个算法执行过程中始终保持正确的搜索区间。以最基本的二分查找为例:
python复制def binary_search(nums, target):
left, right = 0, len(nums) - 1 # 初始搜索区间为整个数组
while left <= right: # 当区间有效时继续
mid = left + (right - left) // 2 # 防止溢出
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1 # 调整左边界
else:
right = mid - 1 # 调整右边界
return -1
这里的关键是理解while left <= right和边界调整时的±1操作。很多力扣题解会提到"左闭右闭"区间概念,这其实就是循环不变量的具体表现。当区间定义为[left, right]时:
- 循环条件应为
left <= right,因为left == right时区间仍然有效 - 当
nums[mid] < target时,新的左边界必须是mid + 1,因为mid已经检查过 - 当
nums[mid] > target时,新的右边界必须是mid - 1,同理
常见误区:将循环条件写成
left < right会导致漏查边界情况,这在力扣的测试用例中经常会遇到边缘case失败。
2.2 三种二分查找模板对比
根据力扣题目的不同需求,二分查找通常有三种变形:
- 标准查找:找到目标值的位置(如上例)
- 左边界查找:找到第一个等于目标值的位置
- 右边界查找:找到最后一个等于目标值的位置
以左边界查找为例(力扣34题):
python复制def left_bound(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return left if left < len(nums) and nums[left] == target else -1
与标准查找的区别在于:
- 当
nums[mid] == target时仍然收缩右边界 - 最终返回的是
left而非mid - 需要检查
left是否越界
3. 力扣经典题型实战解析
3.1 基础题型:704. 二分查找
这是最标准的二分查找实现,力扣通过率高达55%,但仍有几个易错点:
- 循环条件选择:
left <= rightvsleft < right - 中间值计算:
mid = (left + right) // 2可能溢出 - 边界调整:是否需要
±1
正确的Python实现:
python复制class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
实测技巧:在力扣上提交时,故意去掉
left <= right中的等号,可以看到测试用例nums = [5], target = 5会失败,这就是典型的边界条件错误。
3.2 变形题型:35. 搜索插入位置
这道题要求找到目标值应该插入的位置,本质是寻找第一个大于等于目标值的元素位置。可以使用左边界查找模板:
python复制class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return left
关键点:
- 当
target大于所有元素时,left会停在len(nums)位置 - 不需要检查
nums[left] == target,因为题目允许插入位置 - 时间复杂度保持O(log n)
3.3 高阶题型:4. 寻找两个正序数组的中位数
这道困难题需要二分查找的创造性应用。核心思路是通过二分法找到两个数组的划分位置:
python复制class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
if len(nums1) > len(nums2):
nums1, nums2 = nums2, nums1
m, n = len(nums1), len(nums2)
left, right = 0, m
while left <= right:
partitionX = (left + right) // 2
partitionY = (m + n + 1) // 2 - partitionX
maxLeftX = float('-inf') if partitionX == 0 else nums1[partitionX - 1]
minRightX = float('inf') if partitionX == m else nums1[partitionX]
maxLeftY = float('-inf') if partitionY == 0 else nums2[partitionY - 1]
minRightY = float('inf') if partitionY == n else nums2[partitionY]
if maxLeftX <= minRightY and maxLeftY <= minRightX:
if (m + n) % 2 == 0:
return (max(maxLeftX, maxLeftY) + min(minRightX, minRightY)) / 2
else:
return max(maxLeftX, maxLeftY)
elif maxLeftX > minRightY:
right = partitionX - 1
else:
left = partitionX + 1
解题要点:
- 确保第一个数组长度不大于第二个数组(减少搜索范围)
- 使用二分法确定第一个数组的划分点,第二个数组划分点随之确定
- 检查划分是否满足交叉条件(maxLeftX ≤ minRightY且maxLeftY ≤ minRightX)
- 根据数组总长度奇偶性返回中位数
4. 二分查找的隐藏应用场景
4.1 旋转排序数组问题
力扣33题和81题都是在旋转排序数组中进行搜索,这类问题的突破点在于:
- 旋转后的数组一定有一半是有序的
- 通过比较
nums[mid]和nums[left]可以判断哪边是有序的 - 然后检查目标值是否在有序的那一半中
示例代码(力扣33题):
python复制class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
# 左半部分有序
if nums[left] <= nums[mid]:
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
# 右半部分有序
else:
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1
4.2 在无限序列中查找
力扣702题(会员题)要求在不知道长度的排序数组中查找元素,这需要动态调整搜索边界:
python复制class Solution:
def search(self, reader, target):
left, right = 0, 1
# 先找到可能包含target的区间
while reader.get(right) < target:
left = right
right *= 2
# 标准二分查找
while left <= right:
mid = left + (right - left) // 2
val = reader.get(mid)
if val == target:
return mid
elif val < target:
left = mid + 1
else:
right = mid - 1
return -1
4.3 二分答案法
有些问题看似与搜索无关,但可以通过对答案进行二分查找来解决。例如力扣410题"分割数组的最大值":
python复制class Solution:
def splitArray(self, nums: List[int], m: int) -> int:
def can_split(max_sum):
count = 1
current = 0
for num in nums:
current += num
if current > max_sum:
count += 1
current = num
if count > m:
return False
return True
left, right = max(nums), sum(nums)
while left < right:
mid = left + (right - left) // 2
if can_split(mid):
right = mid
else:
left = mid + 1
return left
这种方法的时间复杂度是O(n log S),其中S是数组元素的和。
5. 二分查找的常见陷阱与调试技巧
5.1 死循环问题
二分查找最常见的bug是陷入死循环,通常由以下原因导致:
- 边界更新不正确:忘记
±1或错误地更新边界 - 循环条件选择不当:
left < rightvsleft <= right - 中间值计算错误:
mid = (left + right) // 2可能溢出
调试方法:
- 打印每次循环的
left, mid, right值 - 使用小测试用例手动模拟执行过程
- 特别注意
left和right相邻时的情况
5.2 边界条件错误
力扣的测试用例经常包含以下边界情况:
- 空数组
- 单元素数组
- 目标值小于所有元素
- 目标值大于所有元素
- 目标值不存在但位于数组范围内
- 有重复元素的数组
应对策略:
- 显式处理特殊case(如空数组)
- 在返回前检查索引是否越界
- 使用断言验证不变量
5.3 力扣刷题实战建议
- 按照专题刷题:先集中刷完二分查找相关题目(约20道)
- 总结模板:整理出自己熟悉的代码模板
- 分类练习:将题目分为标准查找、边界查找、旋转数组等类别
- 反复练习:对易错题目进行多次练习
- 参加周赛:在时间压力下检验掌握程度
我个人的刷题路线建议:
- 基础:704、35、34
- 旋转数组:33、81、153
- 二分答案:410、875、1011
- 特殊应用:378、668、702
- 综合挑战:4、240
最后分享一个调试二分查找的实用技巧:在循环内部添加打印语句,观察搜索区间的变化过程。这在处理复杂变形时尤其有用,可以帮助快速定位逻辑错误。记住,二分查找的难点不在于写出能运行的代码,而在于写出对所有边界条件都正确的代码。
