🔍 二分查找 — 精准定位的艺术

O(log n)的搜索奇迹,每一步砍掉一半可能性

📖 二分查找的核心概念

二分查找(Binary Search)是一种在有序序列中高效查找的算法。每一步将搜索范围缩小一半,时间复杂度为 O(log n)。虽然概念简单,但边界条件的处理是面试中最容易出错的环节之一——据统计,90% 的程序员无法一次写出正确的二分查找。

二分查找的核心流程: 有序数组: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19] 目标: 11 第1轮: [1, 3, 5, 7, 9, |11|, 13, 15, 17, 19] left=0 mid=4 right=9 nums[4]=9 < 11 → 搜索右半部分 第2轮: [_, _, _, _, _, |11|, 13, 15, 17, 19] left=5 mid=7 right=9 nums[7]=15 > 11 → 搜索左半部分 第3轮: [_, _, _, _, _, |11|, 13, _, _, _] left=5 mid=5 right=6 nums[5]=11 = 11 → 找到!返回 5 每一步砍掉一半 → O(log n)

1. 两种二分模板

模板一:精确查找(找目标值)
循环条件:left <= right
找到目标直接返回,否则返回 -1
适用:确认目标值是否存在
模板二:边界查找(找左边界/右边界)
循环条件:left < right
找到目标不返回,继续缩小范围
适用:找第一个 ≥ target 的位置、找最后一个 ≤ target 的位置

2. 二分查找的易错点

  1. mid = left + (right - left) // 2,不用 (left + right) // 2 避免溢出
  2. 循环条件:<= 还是 <?取决于区间定义(闭区间还是半开区间)
  3. 更新边界:left = mid + 1 还是 left = mid?取决于 mid 是否可能为答案
  4. 死循环:当 left = right - 1 时,如果 left = mid,mid 仍等于 left,死循环!

3. 何时使用二分查找?

二分查找不限于有序数组!核心前提是搜索空间具有单调性——即存在一个分界点,一侧满足条件,另一侧不满足。

场景单调性体现示例
有序数组查找值单调递增LC 35, LC 704
旋转数组查找两段分别有序LC 33
求第K小/大答案单调LC 378
最小化最大值判定函数单调LC 410
求局部峰值相邻差单调LC 162

🎯 题目一:搜索插入位置 (LC 35)

题目描述:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

示例: nums = [1, 3, 5, 6], target = 5 → 返回 2 示例: nums = [1, 3, 5, 6], target = 2 → 返回 1 示例: nums = [1, 3, 5, 6], target = 7 → 返回 4 核心思想: 找第一个 ≥ target 的位置 这就是"左边界"查找!

思路分析

左边界二分:找第一个 ≥ target 的位置。
1. 如果 target 存在,就是 target 的位置
2. 如果 target 不存在,就是插入位置
循环结束后,left 就是答案。
时间复杂度:O(log n),空间复杂度:O(1)

代码实现

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:
                return mid
            elif nums[mid] < target:
                left = mid + 1
            else:
                right = mid - 1
        return left

复杂度分析

时间复杂度:O(log n) — 每次砍半

空间复杂度:O(1) — 只用几个变量

变体练习

🎯 题目二:搜索旋转排序数组 (LC 33)

题目描述:整数数组 nums 按升序排列,数组中的值互不相同,在传递给函数之前在某个下标处旋转。给你旋转后的数组和一个目标值 target,如果 nums 中存在这个目标值,返回它的下标,否则返回 -1。

示例: nums = [4, 5, 6, 7, 0, 1, 2], target = 0 旋转后: 两段分别有序 [4, 5, 6, 7 | 0, 1, 2] 第一段↑ 第二段↑ 二分时判断哪一半有序: mid=3, nums[3]=7, nums[0]=4 7 ≥ 4 → 左半 [4,5,6,7] 有序 target=0 < 4 → target不在左半 → 搜索右半 mid=5, nums[5]=1, nums[4]=0 1 ≥ 0 → 左半 [0,1] 有序 target=0 在 [0,1] 中 → 搜索左半 mid=4, nums[4]=0 = target → 找到! 返回4

思路分析

旋转数组二分:关键观察——旋转数组从 mid 切开,必有一半是有序的。
1. 比较 nums[mid] 和 nums[left],判断哪一半有序
2. 如果左半有序(nums[mid] ≥ nums[left]):检查 target 是否在左半范围
3. 如果右半有序:检查 target 是否在右半范围
4. 每次排除一半,O(log n)
时间复杂度:O(log n),空间复杂度:O(1)

代码实现

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[mid] >= nums[left]:  # 左半有序
                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

复杂度分析

时间复杂度:O(log n) — 标准二分

空间复杂度:O(1)

有重复元素的旋转数组(LC 81)更复杂:当 nums[mid] == nums[left] 时无法判断哪半有序,需要 left++ 逐步缩小范围,最坏 O(n)。

变体练习

🎯 题目三:寻找峰值 (LC 162)

题目描述:给定一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,返回任何一个峰值的位置即可。你可以假设 nums[-1] = nums[n] = -∞

示例: nums = [1, 2, 3, 1] 位置: 0 1 2 3 数值: [1, 2, 3, 1] ↑ 峰值! 3 > 2 且 3 > 1 二分思路: 如果 nums[mid] < nums[mid+1] → 右侧一定有峰值 → left = mid + 1 如果 nums[mid] > nums[mid+1] → mid可能是峰值 → right = mid 因为边界是-∞,所以"爬坡"一定能到峰值!

思路分析

爬坡二分:这道题数组不有序,但依然可以用二分!
核心观察:比较 nums[mid] 和 nums[mid+1]:
- 如果上升(nums[mid] < nums[mid+1]),右侧必有峰值(因为右边最终会下降到 -∞)
- 如果下降(nums[mid] > nums[mid+1]),mid 可能就是峰值,或左侧有峰值
- 这就是"单调性"——虽然数组不单调,但"峰值的存在性"有单调性
时间复杂度:O(log n),空间复杂度:O(1)

代码实现

class Solution:
    def findPeakElement(self, nums: List[int]) -> int:
        left, right = 0, len(nums) - 1
        while left < right:
            mid = left + (right - left) // 2
            if nums[mid] < nums[mid + 1]:
                left = mid + 1
            else:
                right = mid
        return left

复杂度分析

时间复杂度:O(log n)

空间复杂度:O(1)

这道题是"二分不限于有序数组"的经典例子!关键在于发现搜索空间的单调性:mid 和 mid+1 的大小关系决定了峰值在哪一侧。

变体练习

成就解锁:二分猎人 — 掌握精确查找、边界查找、旋转数组和峰值查找四种二分变形
LeetCode AC验证:LC 35 Search Insert Position ✅ | LC 33 Search in Rotated Sorted Array ✅ | LC 162 Find Peak Element ✅

📝 课后练习

  1. LC 704 二分查找(基础模板练习)
  2. LC 34 排序数组中查找首末位置(左右边界)
  3. LC 69 x 的平方根(二分猜答案)
  4. LC 367 有效的完全平方数
  5. LC 410 分割数组的最大值(二分答案 + 贪心判定)

🔑 本课要点回顾

📚 扩展阅读

思考题:如何用二分查找找到"满足某条件的最小值"?(即二分答案)例如:分割数组的最大值(LC 410),如何将"最小化最大值"转化为二分查找问题?提示:判定"最大值 ≤ mid 是否可行"具有单调性。