二分查找是面试中出现频率最高的算法之一:代码不到十行,却藏着无数边界陷阱。本文从最朴素的版本讲起,带你写出一份"一次通过"的二分查找,并掌握它背后的思维模式——对"单调性"的利用。

1. 核心思想

在有序数组中查找目标值,每次取中间元素比较,把搜索区间砍掉一半。时间复杂度 O(log n):10 亿个元素也只需约 30 次比较。前提只有一个——数组必须有序

def binary_search(nums, target):
    left, right = 0, len(nums) - 1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] == target:
            return mid
        elif nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1  # 未找到

print(binary_search([1, 3, 5, 7, 9, 11], 7))  # 3
print(binary_search([1, 3, 5, 7, 9, 11], 4))  # -1

2. 三个关键细节

3. 变体:查找左边界

当数组中有重复元素,题目往往要求"第一个等于 target 的位置"。此时找到目标后不立即返回,而是继续向左收缩:

def find_left(nums, target):
    left, right = 0, len(nums) - 1
    ans = -1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] >= target:
            ans = mid
            right = mid - 1
        else:
            left = mid + 1
    return ans if ans != -1 and nums[ans] == target else -1

print(find_left([1, 2, 2, 2, 3, 4], 2))  # 1
print(find_left([1, 2, 3, 4], 5))        # -1

对称地,把 nums[mid] >= target 改成 nums[mid] > target,就得到"第一个大于 target"的位置——这是求右边界和插入位置的通用套路。

4. 经典进阶:旋转数组

面试升级题:"升序数组在某点旋转(如 [4,5,6,1,2,3]),如何 O(log n) 找最小值?"思路:比较 mid 与右端点的值,判断哪一半是有序的,然后向"断裂处"收缩。

def find_min(nums):
    left, right = 0, len(nums) - 1
    while left < right:
        mid = (left + right) // 2
        if nums[mid] > nums[right]:
            left = mid + 1   # 最小值在右半段
        else:
            right = mid      # 最小值在左半段(含 mid)
    return nums[left]

print(find_min([4, 5, 6, 1, 2, 3]))  # 1

注意这里循环条件变成了 left < right,且 right 收缩时不跳过 mid——两种模板的适用场景不同,一定要理解而不是死记。

5. 何时想到二分

💡 学习建议:把"标准版、左边界、右边界、旋转数组"四个模板各默写五遍,直到能闭眼写出;面试时先问清楚"有无重复元素、要找哪个边界",再动手。