查找与二分

从「一个个找」到「对半砍」。 在无序序列里找某个值,只能从头扫到尾,线性查找 O(n)。若序列有序,就可以每次看中间:比目标大就往左半找,比目标小就往右半找,一次排除一半——这就是二分查找,只需 O(log n) 次比较。二分不仅用于「找等于」,还能找「第一个等于」「最后一个等于」「第一个大于等于」等变体,以及「答案二分」:在满足单调性的前提下,二分答案再检查。本章讲线性查找、二分的基本写法与边界、常见变体、旋转数组与答案二分,配图示与至少三道例题的 Python 代码(类型标注与注释)。

一、线性查找

在长度为 n 的序列中找目标值,若不保证有序,只能顺序扫描,最坏比较 n 次,O(n)。若找到则返回下标,否则返回 -1 或插入位置。线性查找也用于找最大值、最小值、满足条件的第一个元素等。

线性查找:无序时只能从左到右逐个比较
from typing import List


def linear_search(arr: List[int], target: int) -> int:
    """线性查找:返回 target 的下标,不存在返回 -1。"""
    for i in range(len(arr)):
        if arr[i] == target:
            return i
    return -1

二、二分查找的前提与基本写法

前提:序列有序(或具有某种单调性),才能根据「中间值与目标的大小关系」排除一半。区间约定:常用左闭右开 [left, right),即有效下标为 left..right-1;循环条件为 left < right,缩区间时 right = mid 或 left = mid + 1,避免死循环。mid 防溢出:mid = left + (right - left) // 2。

有序数组 [2,5,8,11,14,17,20,23]:二分查找 14 的过程
from typing import List


def binary_search(arr: List[int], target: int) -> int:
    """
    标准二分查找:有序数组中找到 target 的下标,不存在返回 -1。
    左闭右开 [left, right),mid = left + (right - left) // 2。
    """
    left, right = 0, len(arr)
    while left < right:
        mid = left + (right - left) // 2
        if arr[mid] == target:
            return mid
        if arr[mid] < target:
            left = mid + 1
        else:
            right = mid
    return -1

三、二分查找变体

第一个等于 target:当 arr[mid] == target 时不要立刻返回,令 right = mid 继续往左找;最后 left 为「第一个等于」的下标(若不存在则 left 为第一个大于 target 的下标)。最后一个等于 target:相等时令 left = mid + 1 往右挤,最后 left - 1 或 right - 1 为「最后一个等于」。第一个 ≥ target(lower_bound):比较时 arr[mid] < target 则 left = mid + 1,否则 right = mid;退出时 left 即为第一个 ≥ target 的位置。最后一个 ≤ target:可转化为「第一个 > target」减 1,或对称写。这些变体的循环不变式要一致,避免边界错误。

from typing import List


def lower_bound(arr: List[int], target: int) -> int:
    """
    第一个 >= target 的下标;若都 < target 则返回 len(arr)。
    左闭右开,arr[mid] < target 则 left = mid+1,否则 right = mid。
    """
    left, right = 0, len(arr)
    while left < right:
        mid = left + (right - left) // 2
        if arr[mid] < target:
            left = mid + 1
        else:
            right = mid
    return left


def upper_bound(arr: List[int], target: int) -> int:
    """
    第一个 > target 的下标;若都 <= target 则返回 len(arr)。
    """
    left, right = 0, len(arr)
    while left < right:
        mid = left + (right - left) // 2
        if arr[mid] <= target:
            left = mid + 1
        else:
            right = mid
    return left


def search_range(arr: List[int], target: int) -> List[int]:
    """返回 target 的起始与结束下标 [start, end],不存在则 [-1, -1]。"""
    lo = lower_bound(arr, target)
    if lo == len(arr) or arr[lo] != target:
        return [-1, -1]
    hi = upper_bound(arr, target) - 1
    return [lo, hi]

四、搜索插入位置

在有序数组中找 target 的插入位置,使插入后仍有序。即第一个 ≥ target 的下标,即 lower_bound。若 target 已存在,插入在其左侧或右侧依题意;通常理解为「第一个 ≥ target」则插入在相同元素最左侧。

五、旋转数组与答案二分

旋转数组:有序数组 [a0, a1, …, an-1] 从某处截断并前后对调,如 [4,5,6,7,0,1,2]。仍可二分:mid 与某侧(如 right)比较,判断 mid 在「前半段」还是「后半段」,再判断 target 在 mid 左还是右。答案二分:若「答案 x 满足条件」具有单调性(如 x 可行则比 x 更大/更小也可行),可二分答案,再在 O(f(n)) 内检查是否可行,总时间 O(f(n) log 范围)。典型:x 的平方根、满足条件的最小/最大值。

旋转排序数组:两段各自有序,边界在最小值处
from typing import List


def search_rotated(arr: List[int], target: int) -> int:
    """
    旋转排序数组(无重复)中找 target。比较 nums[mid] 与 nums[right] 判断 mid 在左半还是右半,
    再判断 target 在 mid 左还是右。
    """
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = left + (right - left) // 2
        if arr[mid] == target:
            return mid
        if arr[mid] <= arr[right]:
            if arr[mid] < target <= arr[right]:
                left = mid + 1
            else:
                right = mid - 1
        else:
            if arr[left] <= target < arr[mid]:
                right = mid - 1
            else:
                left = mid + 1
    return -1

六、小结

线性查找 O(n),适用于无序。二分查找要求有序,左闭右开、mid 防溢出,O(log n)。变体:lower_bound(第一个 ≥)、upper_bound(第一个 >)、search_range。旋转数组通过 mid 与端点比较判断所在段再二分。答案二分在单调性下二分答案再检查。下一章讲递归与分治

七、例题

以下例题使用 Python 3 类型标注与注释。

例题 1:搜索插入位置

题目描述:有序无重复数组 numstarget,若 target 存在返回其下标,否则返回插入后保持有序的插入位置。

思路:即 lower_bound:第一个 ≥ target 的下标。

from typing import List


def search_insert(nums: List[int], target: int) -> int:
    """第一个 >= target 的下标,即 lower_bound。"""
    left, right = 0, len(nums)
    while left < right:
        mid = left + (right - left) // 2
        if nums[mid] < target:
            left = mid + 1
        else:
            right = mid
    return left

讲解:标准 lower_bound,时间 O(log n),空间 O(1)。

例题 2:在旋转排序数组中搜索

题目描述:旋转排序数组(无重复)与 target,返回 target 的下标,不存在返回 -1。

from typing import List


def search_rotated_sorted(nums: List[int], target: int) -> int:
    """
    nums[mid] 与 nums[right] 比较:若 nums[mid] <= nums[right],则 [mid, right] 有序;
    否则 [left, mid] 有序。在有序的那半里判断 target 是否在范围内,缩区间。
    """
    left, right = 0, len(nums) - 1
    while left <= right:
        mid = left + (right - left) // 2
        if nums[mid] == target:
            return mid
        if nums[mid] <= nums[right]:
            if nums[mid] < target <= nums[right]:
                left = mid + 1
            else:
                right = mid - 1
        else:
            if nums[left] <= target < nums[mid]:
                right = mid - 1
            else:
                left = mid + 1
    return -1

讲解:先判断 mid 落在前半段还是后半段(与 right 比),再在有序的那一段里判断 target 是否在 [mid, right] 或 [left, mid],缩小区间。时间 O(log n)。

例题 3:x 的平方根(答案二分)

题目描述:非负整数 x,返回 x 的算术平方根(向下取整)。不允许使用内置开方。

思路:答案 k 满足 k² ≤ x 且 (k+1)² > x。二分 k ∈ [0, x],检查 mid*mid ≤ x 则候选为 mid 或更大,否则更小。

def my_sqrt(x: int) -> int:
    """
    二分答案:找最大的 k 使得 k*k <= x。左闭右开,mid*mid <= x 则 left = mid+1 尝试更大,否则 right = mid。
    返回 left - 1(最后一个满足 k*k <= x 的 k)。
    """
    if x <= 1:
        return x
    left, right = 0, x + 1
    while left < right:
        mid = left + (right - left) // 2
        if mid * mid <= x:
            left = mid + 1
        else:
            right = mid
    return left - 1

讲解:二分 k,满足 k² ≤ x 的 k 构成「可行」集合,取最大即最后一个可行。时间 O(log x),空间 O(1)。

例题 4:搜索范围

题目描述:非降序数组 numstarget,返回 target 的起始与结束下标 [start, end];不存在则 [-1, -1]。

from typing import List


def search_range(nums: List[int], target: int) -> List[int]:
    """lower_bound 得第一个 >= target;upper_bound 得第一个 > target,则 [lo, hi-1] 为范围。"""
    def lower_bound(t: int) -> int:
        lo, hi = 0, len(nums)
        while lo < hi:
            mid = lo + (hi - lo) // 2
            if nums[mid] < t:
                lo = mid + 1
            else:
                hi = mid
        return lo

    lo = lower_bound(target)
    if lo == len(nums) or nums[lo] != target:
        return [-1, -1]
    hi = lower_bound(target + 1) - 1  # 第一个 > target 的前一个即最后一个 == target
    return [lo, hi]

讲解:第一个等于 target 即 lower_bound(target);最后一个等于即「第一个大于 target」减 1,可用 lower_bound(target+1)-1。时间 O(log n)。