Loading......

文章背景图

算法 - 二分查找专项

2026-04-12
3
-
- 分钟
|

算法 - 二分查找专项

核心:红蓝染色法,通过每次排除一半元素,将 O(n) 的暴力搜索优化到 O(logn),是解决有序数组搜索问题的通解。


一、基础知识点(二分查找基础)

1. 二分查找模板(红蓝染色法)

题目描述

给定一个从小到大有序的数组 nums 和目标值 target,找到第一个大于等于 target 的元素下标

暴力解法

  • 思路:从左到右遍历数组,逐一比较每个元素与 target 的大小

  • 最坏情况:target 在数组最右侧,需要遍历全部 n 个元素

  • 复杂度:时间 O(n),空间 O(1)

  • 优化动机:没有利用数组有序这一关键条件

优化解法(二分查找)

核心思想

  1. 利用有序性:数组有序,所以对于任意位置 mid:

    • nums[mid] < target,则 mid 左侧所有元素都 < target(红色)

    • nums[mid] >= target,则 mid 右侧所有元素都 >= target(蓝色)

  2. 红蓝染色法:将数组想象成两部分:

    • 红色区域:L 左侧(不含 L),所有元素 < target

    • 蓝色区域:R 右侧(不含 R),所有元素 >= target

    • 我们的目标:找到第一个蓝色元素的位置

  3. 循环不变量(核心!):

    • L 左侧(索引 < L)→ 一定是红色(< target)

    • R 右侧(索引 > R)→ 一定是蓝色(>= target)

    • 每轮循环后,搜索区间 [L, R] 缩小,但不变量保持不变

算法步骤

  1. 初始化L = 0R = n - 1,搜索区间为 [L, R](左闭右闭)

  2. 取中点mid = (L + R) // 2(向下取整)

  3. 判断并缩小区间

    • nums[mid] < target:蓝色在右侧,L = mid + 1(mid 及其左侧都是红色)

    • nums[mid] >= target:蓝色在当前位置或左侧,R = mid - 1

  4. 终止条件L > R 时循环结束

  5. 返回结果L(即 R + 1)就是第一个蓝色元素的位置

为什么 L = mid + 1 而不是 L = mid

关键nums[mid] < target 时,mid 本身一定是红色(< target),所以可以直接跳过,搜索区间变为 [mid+1, R]。如果写成 L = mid,当区间只剩一个元素时会死循环。

图解示例

数组 nums = [1, 3, 5, 7]target = 6

初始状态:[1, 3, 5, 7]     L=0 R=3
              ↑
             mid=1, nums[1]=3 < 6 → L更新为2

状态2:    [1, 3, 5, 7]        L=2 R=3
                 ↑
                mid=2, nums[2]=5 < 6 → L更新为3

状态3:    [1, 3, 5, 7]           L=3 R=3
                 ↑
                mid=3, nums[3]=7 >= 6 → R更新为2

终止:L=3, R=2 → L > R,退出循环
返回 L=3,即第一个 >= 6 的元素是 nums[3]=7

代码实现

def lower_bound(nums, target):
    """
    查找第一个 >= target 的元素下标
    :param nums: 有序数组(从小到大)
    :param target: 目标值
    :return: 第一个 >= target 的下标,如果不存在返回数组长度
    """
    n = len(nums)
    # 初始化:L=0 指向搜索区间左端,R=n-1 指向右端
    # 此时 L-1(索引-1,不存在)可视为红色边界
    # R+1(索引n,不存在)可视为蓝色边界
    left, right = 0, n - 1

    # 循环条件:left <= right 表示搜索区间不为空
    # 循环不变量始终成立:
    #   - 索引 < left 的元素都 < target(红色)
    #   - 索引 > right 的元素都 >= target(蓝色)
    while left <= right:
        # 防止 (left + right) 溢出的写法
        # mid = (left + right) // 2  # 可能溢出
        mid = left + (right - left) // 2  # 安全写法

        if nums[mid] < target:
            # nums[mid] < target,说明 mid 及左侧都是红色
            # 蓝色区域一定在 [mid+1, right],更新 left
            left = mid + 1
        else:
            # nums[mid] >= target,说明蓝色区域在 [left, mid]
            # 更新 right 为 mid-1,继续在左半部分找第一个蓝色
            right = mid - 1

    # 循环结束后,left 指向第一个蓝色元素的位置
    # 此时 left == right + 1
    # 如果所有元素都 < target,left 会等于 n
    return left

三种区间写法对比

写法

初始化

循环条件

L 更新

R 更新

返回值

左闭右闭 [L, R]

L=0, R=n-1

L <= R

mid+1

mid-1

L

左闭右开 [L, R)

L=0, R=n

L < R

mid+1

mid

L

双开 (L, R)

L=-1, R=n

L+1 < R

mid

mid

R

推荐使用左闭右闭写法,最符合常规思维,不易出错。

四种类型转换(重要技巧)

要找的条件

转换方法

示例

>= target

直接用 lower_bound

target=8 → 找 >= 8

> target

转换为 >= (target+1)

target=8 → 找 >= 9

<= target

转换为 >= (target+1) 的左侧

target=8 → 找 >= 9 的位置,再减 1

< target

转换为 >= target 的左侧位置

target=8 → 找 >= 8 的位置,再减 1

核心思想:四种类型都可以统一用「找第一个 >= x 的位置」来解决。

复杂度

  • 时间:O(logn),每次循环将搜索区间缩小一半

  • 空间:O(1),仅使用常数个变量

关键细节

  • 溢出问题mid = left + (right - left) // 2 防止 left+right 溢出

  • 终止条件left <= right(左闭右闭)或 left < right(左闭右开)

  • 返回值:循环结束后 L 指向第一个蓝色位置

  • 空数组处理:n=0 时,直接返回 0(符合预期)

  • 所有元素都 < target:L 会等于 n

  • 所有元素都 >= target:R 会等于 -1


2. 在有序数组中找开始位置和结束位置

题目描述

给定一个有序数组,找到等于 target 的元素的开始位置和结束位置。如果不存在返回 [-1, -1]

核心思路

这道题本质上是两次二分查找

  • 第一次:找第一个 >= target 的位置(start)

  • 第二次:找第一个 > target 的位置,然后减 1(end)

代码实现

class Solution:
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        n = len(nums)

        # 第一次二分:找第一个 >= target 的位置(start)
        start = 0
        right = n - 1
        while start <= right:
            mid = start + (right - start) // 2
            if nums[mid] < target:
                start = mid + 1
            else:
                right = mid - 1
        # start 就是第一个 >= target 的位置
        # 但需要验证这个位置是否真的等于 target
        if start == n or nums[start] != target:
            return [-1, -1]

        # 第二次二分:找第一个 > target 的位置(end+1)
        end = 0
        right = n - 1
        while end <= right:
            mid = end + (right - end) // 2
            if nums[mid] <= target:
                end = mid + 1
            else:
                right = mid - 1
        # end 是第一个 > target 的位置,所以 end-1 是最后一个 = target 的位置
        return [start, end - 1]

二、练习(3道二分查找题目)

1. 2529. 正整数和负整数的最大计数 🔢

题目描述(LeetCode 2529 - 简单)

给你一个按 非递减顺序 排列的数组 nums,返回正整数数目和负整数数目中的最大值。换句话说,如果 nums 中正整数的数目是 pos,而负整数的数目是 neg,返回 pos 和 neg 二者中的最大值。

注意:0 既不是正整数也不是负整数。

示例

输入:nums = [-2,-1,-1,1,2,3]
输出:3
解释:共有 3 个正整数和 3 个负整数。计数得到的最大值是 3。

解题思路

  • 分别用二分查找找到:

    • 第一个 >= 0 的位置 → 负数个数 = 该位置

    • 第一个 > 0 的位置 → 正数个数 = n - 该位置

  • 两次 lower_bound 取最大值

复杂度

  • 时间:O(logn)(两次二分)

  • 空间:O(1)

Python 代码

class Solution:
    def maximumCount(self, nums: List[int]) -> int:
        n = len(nums)

        # 第一次二分:找第一个 >= 0 的位置(负数分界点)
        l, r = 0, n - 1
        pos1 = n  # 默认值:如果所有数都 < 0,负数个数为 n
        while l <= r:
            mid = (l + r) // 2
            if nums[mid] >= 0:
                pos1 = mid      # 记录第一个 >= 0 的位置
                r = mid - 1     # 继续在左半部分找
            else:
                l = mid + 1
        neg = pos1  # 索引 [0, pos1) 都是负数

        # 第二次二分:找第一个 > 0 的位置(正数分界点)
        l, r = 0, n - 1
        pos2 = n  # 默认值:如果所有数都 <= 0,正数个数为 0
        while l <= r:
            mid = (l + r) // 2
            if nums[mid] > 0:
                pos2 = mid      # 记录第一个 > 0 的位置
                r = mid - 1     # 继续在左半部分找
            else:
                l = mid + 1
        pos = n - pos2  # 索引 [pos2, n) 都是正数

        return max(neg, pos)

2. 2300. 咒语和药水的成功对数 🎯

题目描述(LeetCode 2300 - 中等)

给你两个正整数数组 spells 和 potions,长度分别为 n 和 m,其中 spells[i] 表示第 i 个咒语的能量强度,potions[j] 表示第 j 瓶药水的能量强度。

同时给你一个整数 success。一个咒语和药水的能量强度相乘如果 大于等于 success,那么它们视为一对成功的组合。

请你返回一个长度为 n 的整数数组 pairs,其中 pairs[i] 是能跟第 i 个咒语成功组合的药水数目。

示例

输入:spells = [5,1,3], potions = [1,2,3,4,5], success = 7
输出:[4,0,3]
解释:

  • 第 0 个咒语:5 * [1,2,3,4,5] = [5,10,15,20,25],总共 4 个成功组合

  • 第 1 个咒语:1 * [1,2,3,4,5] = [1,2,3,4,5],总共 0 个成功组合

  • 第 2 个咒语:3 * [1,2,3,4,5] = [3,6,9,12,15],总共 3 个成功组合

解题思路

  1. 先对 potions 数组排序(为二分查找做准备)

  2. 对于每个咒语 spell,求最小药水强度满足 spell * potion >= success

    • 推导:spell * potion >= successpotion >= success / spell

  3. 用二分查找找第一个大于等于 success / spell 的 potions 位置

  4. 成功组合数 = m - 该位置索引(该位置之后都满足)

复杂度

  • 时间:O((n+m) log m)

  • 空间:O(m)(排序占用)

Python 代码

class Solution:
    def successfulPairs(self, spells: List[int], potions: List[int], success: int) -> List[int]:
        potions.sort()  # 排序,为二分查找做准备
        n = len(spells)
        m = len(potions)
        pairs = [0] * n

        for x, y in enumerate(spells):
            # 对于当前咒语 y,计算满足条件的最小药水强度
            # spell * potion >= success → potion >= success / spell
            # 找第一个 >= success/y 的位置
            left, right = 0, m - 1
            while left < right:
                mid = (left + right) // 2
                if potions[mid] * y >= success:
                    right = mid  # 满足条件,继续在左半部分找更小的
                else:
                    left = mid + 1  # 不满足,左移右边界

            # 判断找到的位置是否真的满足条件
            if potions[left] * y >= success:
                pairs[x] = m - left  # 从该位置到末尾都满足
            else:
                pairs[x] = 0

        return pairs

3. 1385. 两个数组间的距离值 📐

题目描述(LeetCode 1385 - 简单)

给你两个整数数组 arr1,arr2 和一个整数 d,请你返回两个数组之间的距离值

「距离值」定义为符合此距离要求的元素数目:对于元素 arr1[i],不存在任何元素 arr2[j] 满足 |arr1[i] - arr2[j]| <= d

示例

输入:arr1 = [4,5,8], arr2 = [10,9,1,8], d = 2
输出:2
解释:

  • arr1[0]=4:|4-10|=6 > d, |4-9|=5 > d, |4-1|=3 > d, |4-8|=4 > d → 符合距离要求

  • arr1[1]=5:|5-10|=5 > d, |5-9|=4 > d, |5-1|=4 > d, |5-8|=3 > d → 符合距离要求

  • arr1[2]=8:|8-10|=2 ≤ d, |8-9|=1 ≤ d, |8-1|=7 > d, |8-8|=0 ≤ d → 不符合距离要求

故而只有 arr1[0] 和 arr1[1] 两个符合距离要求,距离值为 2。

解题思路

  1. 对两个数组分别排序

  2. 对于 arr1 中的每个元素 x,在 arr2 中找第一个 >= x-d 的位置

  3. 判断该位置是否满足 arr2[j] > x + d

  4. 使用双指针:arr2 的指针 j 单调递增,整体 O(n+m)

复杂度

  • 时间:O((n+m) log m)

  • 空间:O(1)

Python 代码

class Solution:
    def findTheDistanceValue(self, arr1: List[int], arr2: List[int], d: int) -> int:
        arr1.sort()  # 排序
        arr2.sort()
        ans = j = 0

        for x in arr1:
            # 在 arr2 中找第一个 >= x-d 的位置
            # 利用 arr2 单调性,指针 j 只增不减
            while j < len(arr2) and arr2[j] < x - d:
                j += 1
            # 判断 arr2[j] 是否超出 x+d 的范围
            # 如果 j 已到末尾,或 arr2[j] > x+d,则 x 符合距离要求
            if j == len(arr2) or arr2[j] > x + d:
                ans += 1

        return ans

三、二分查找算法通用解题总结 ✨

核心适用场景

  • 有序数组中搜索第一个满足某条件的元素位置

  • 统计满足条件的元素个数

统一解题步骤

  1. 明确搜索目标:找第一个 ≥x 的位置,还是找最后一个小于 x 的位置

  2. 确定循环不变量:L-1 一定是红色(不满足),R+1 一定是蓝色(满足)

  3. 选择区间写法:推荐左闭右闭或左闭右开,避免死循环

  4. 根据条件更新边界:mid 满足条件 → 更新 R;不满足 → 更新 L

  5. 返回 L(第一个蓝色位置)

复杂度速记

  • 二分查找:时间 O(logn),空间 O(1)

关键技巧

  • 四种类型互转> target≥ (target+1)≤ target≥ (target+1) 再减 1

  • 乘法转除法:涉及乘法的二分,可将一边除过去(如 2300 题)

  • 单调数组 + 双指针:距离值问题先排序,再用指针跳过不满足条件的区间

  • 溢出处理mid = left + (right - left) // 2 防止 left+right 溢出

评论交流