LEETCODE 704Easy

二分查找

二分的难点不在取中点,而在于始终说清楚搜索区间的定义。

问题拆解

在升序数组里找目标值,找到返回下标,找不到返回 -1,要求 O(log n)

思路本身人人都知道:每次拿中间元素和目标比,砍掉一半。真正让二分写错的从来不是取中点,而是几个边界问题搅在一起——循环条件写 < 还是 <=?更新时用 mid 还是 mid ± 1?这些问题没有孤立的答案,它们全都取决于一件事:你如何定义搜索区间

先定下区间的含义,剩下的每一行代码都只是保持和这个定义一致。

左闭右闭区间

这里采用左闭右闭区间 [left, right]。这意味着左右端点都可能是答案,因此循环条件必须写成 left <= right——当 left == right 时区间里还剩一个候选,仍要检查。

public int search(int[] nums, int target) {
    int left = 0, right = nums.length - 1;

    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            return mid;
        }
        if (nums[mid] < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }

    return -1;
}
def search(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] < target:
            left = mid + 1
        else:
            right = mid - 1

    return -1
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
}
pub fn search(nums: Vec<i32>, target: i32) -> i32 {
    let (mut left, mut right) = (0i32, nums.len() as i32 - 1);

    while left <= right {
        let mid = left + (right - left) / 2;
        match nums[mid as usize].cmp(&target) {
            std::cmp::Ordering::Equal => return mid,
            std::cmp::Ordering::Less => left = mid + 1,
            std::cmp::Ordering::Greater => right = mid - 1,
        }
    }

    -1
}

为什么是 mid ± 1?判断完 nums[mid] 后,已经确定它不是答案。新的搜索区间不应该再次包含 mid,所以向右时从 mid + 1 开始,向左时到 mid - 1 结束。这同时保证区间每轮严格缩小,不会死循环。

取中点写成 left + (right - left) // 2 而不是 (left + right) // 2,是为了在整数会溢出的语言里避免 left + right 越界;Python 没有这个问题,但保持这个习惯没有坏处。

检查清单

  1. 数组是否有序(二分的前提);
  2. 搜索区间是闭区间还是半开区间;
  3. 循环条件是否与区间定义一致;
  4. 更新边界后,区间是否确实缩小。

复杂度

指标 复杂度 原因
时间 O(log n) 每轮把候选区间砍掉一半
空间 O(1) 只用三个下标变量

可以迁移的模式

  • 有序结构上找某个值、找第一个/最后一个满足条件的位置;
  • 更广义地:答案单调(某个条件从 false 翻转为 true)时,可以对答案本身二分;
  • 写任何变体前,先把区间定义和循环不变量说出口,再落笔。

只要区间定义、循环条件、边界更新这三件事前后一致,二分就不需要靠背模板。