目录
正在加载目录…
专栏文章
专栏文章
经典算法专栏
1. 图算法:遍历、最短路、MST、拓扑与 Tarjan 2. 排序与搜索:十大排序、二分查找与动态规划 3. 动态规划:五类经典问题的状态转移方法 4. 业务场景算法:限流、Top-K 与一致性哈希 5. 算法复杂度:递归树、主定理与摊还分析 6. 基础数据结构:树、堆、哈希与并查集 7. 分治与贪心:拆解问题与局部最优策略 8. 字符串算法:KMP、BM、RK 与 Trie 9. 网络流与匹配:最大流、最小割与二分图 10. 二分算法:边界查找与答案二分模板 11. 位运算速查:二进制原理与高频解题模板 11. 并查集:连通性、路径压缩与进阶变体 12. 显式栈与状态机:替代递归的通用方法

二分算法:边界查找与答案二分模板

发布于 2026-07-29 11:45 · 最后编辑于 2026-07-31 15:53 · 字数 3,502 👁 70 次阅读

二分的本质不是“在有序数组里找数”,而是在一个存在单调边界的搜索空间中定位分界点。本文用统一的半开区间模板推导精确查找、前驱/后继、重复元素边界与答案二分,并配套 LeetCode 题型。

目录

章节说明
适用条件与统一模型何时能二分;从“找值”转为“找边界”
区间与循环不变量统一使用 [left, right),避免边界混乱
核心模板lowerBoundupperBound 与精确查找
前驱、后继与重复元素严格小于、最大不大于、最小不小于等
过程图解指针如何收缩,为什么不会漏解
答案二分在值域上找最小/最大可行答案
LeetCode 题型地图从模板到经典题目
常见错误与快速参考卡提交前检查清单

适用条件与统一模型

二分查找要求的并非一定是“数组有序”,而是搜索空间中存在一个可以判断方向的单调性质。若谓词 ok(x) 满足下面任一种形态,就能二分:

第一个 true 的形态:false false false true true true
最后一个 true 的形态:true  true  true  false false false
搜索空间单调谓词要找的边界典型场景
有序数组下标nums[i] >= target第一个 true第一个 ≥ target、插入位置
有序数组下标nums[i] > target第一个 true第一个 > target、右插入位置
整数答案范围canFinish(x)第一个 true最小速度、最小容量、最小天数
整数答案范围canPlace(x)最后一个 true最大最小距离、最大可行值

先写谓词,再写二分。 “答案满足什么条件”比“left 应该怎么移动”更重要;边界移动只是谓词单调性的直接结果。

选模板的决策图

binary search template decision

复杂度与前提

  • 每轮排除至少约一半候选,时间复杂度为 O(log n);只使用常数个变量,空间复杂度为 O(1)
  • 数组二分依赖 O(1) 随机访问;链表取中点是 O(n),通常没有收益。
  • 对答案二分,复杂度是 O(log R × check)R 是答案值域大小,check 是一次可行性检查的成本。

排序与搜索 中“排序 + 搜索”的概览不同,本文将所有变体收敛成边界模板,重点解释不变量与可迁移性。

区间与循环不变量

本文固定使用左闭右开区间 [left, right)left 可以取 0right 可以取 n,即使数组为空也天然成立。

对于“找第一个满足 predicate(i) 的位置”,维护:

  1. [0, left) 中的元素都不满足谓词;
  2. [right, n) 中的元素都满足谓词;
  3. 尚未确定的候选只在 [left, right)

left == right,候选区间为空,二者正好相遇在第一个满足谓词的位置;若整个数组都不满足,则返回 n

// 所有下标均在 [0, n);right = n 是合法的“尾后位置”。
int left = 0;
int right = n;
while (left < right) {
    int mid = left + (right - left) / 2;
    if (predicate(mid)) {
        right = mid;      // mid 可能正是第一个满足者,不能丢弃
    } else {
        left = mid + 1;   // mid 已确认不满足,安全排除
    }
}
return left;

left < right 配合 [left, right)left <= right 配合 [left, right] 两套写法都正确,但不能混用初始化、循环条件与更新规则。

核心模板

lowerBound:第一个 >= target

lowerBound 返回第一个不小于 target 的下标,也就是 target 的左插入位置;所有元素都小于目标时返回 n

/** 返回第一个满足 nums[index] >= target 的 index,范围为 [0, nums.length]。 */
int lowerBound(int[] nums, int target) {
    int left = 0;
    int right = nums.length;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] >= target) {
            right = mid;
        } else {
            left = mid + 1;
        }
    }
    return left;
}

upperBound:第一个 > target

只需将谓词改为 nums[mid] > target。它返回 target 的右插入位置。

/** 返回第一个满足 nums[index] > target 的 index,范围为 [0, nums.length]。 */
int upperBound(int[] nums, int target) {
    int left = 0;
    int right = nums.length;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > target) {
            right = mid;
        } else {
            left = mid + 1;
        }
    }
    return left;
}

找某个值:先找左边界,再验证

“精确查找”可以直接遇到相等就返回,但出现重复元素时返回位置不稳定。工程和刷题中更推荐复用 lowerBound:结果确定,还能无缝扩展到首个匹配位置。

/** 找 target 第一次出现的位置;不存在返回 -1。 */
int findFirstEqual(int[] nums, int target) {
    int index = lowerBound(nums, target);
    return index < nums.length && nums[index] == target ? index : -1;
}

/** 是否包含 target。 */
boolean contains(int[] nums, int target) {
    return findFirstEqual(nums, target) != -1;
}
目标直接使用的边界额外验证
是否存在 targetlowerBound(target)下标未越界且值相等
第一个等于 targetlowerBound(target)同上
最后一个等于 targetupperBound(target) - 1下标未越界且值相等
target 出现次数upperBound(target) - lowerBound(target)无需单独扫描

前驱、后继与重复元素

给定升序数组,前驱是小于(或不大于)目标的最大元素,后继是大于(或不小于)目标的最小元素。不要把“严格”和“可相等”混淆。

要找的值对应下标无答案的条件
第一个 >= target(后继,允许相等)lowerBound(target)下标为 n
第一个 > target(严格后继)upperBound(target)下标为 n
最大 < target(严格前驱)lowerBound(target) - 1下标为 -1
最大 <= target(前驱,允许相等)upperBound(target) - 1下标为 -1
/** 最大严格小于 target 的值;没有则返回 null。 */
Integer predecessorLessThan(int[] nums, int target) {
    int index = lowerBound(nums, target) - 1;
    return index >= 0 ? nums[index] : null;
}

/** 最小大于等于 target 的值;没有则返回 null。 */
Integer successorAtLeast(int[] nums, int target) {
    int index = lowerBound(nums, target);
    return index < nums.length ? nums[index] : null;
}

示例:找比 4 小的最大值

数组 nums = [1, 2, 4, 4, 4, 7, 9],目标为 4lowerBound(4) = 2,所以最大严格小于 4 的下标是 2 - 1 = 1,答案为 2

若题目改成“找不大于 4 的最大值”,则用 upperBound(4) - 1 = 5 - 1 = 4,答案才是 4

index:  0  1  2  3  4  5  6
nums : [1, 2, 4, 4, 4, 7, 9]
              ↑                 lowerBound(4) = 2:第一个 >= 4
           ↑                    最大 < 4:index = 1,值为 2
                    ↑           upperBound(4) = 5:第一个 > 4
                 ↑              最大 <= 4:index = 4,值为 4

过程图解

lowerBound([1, 2, 4, 4, 4, 7, 9], 4) 为例。谓词是 nums[i] >= 4,其真假分布为 F F T T T T T

初始: [ left = 0,                 right = 7 )
       [ 1, 2, 4, 4, 4, 7, 9 ]
                  mid = 3,nums[3] = 4,满足,所以 right = 3

第 2 轮:[ left = 0, right = 3 )
         [ 1, 2, 4 | 4, 4, 7, 9 ]
            mid = 1,nums[1] = 2,不满足,所以 left = 2

第 3 轮:[ left = 2, right = 3 )
         [ 1, 2, 4 | 4, 4, 7, 9 ]
               mid = 2,nums[2] = 4,满足,所以 right = 2

结束: left == right == 2,答案为第一个满足谓词的位置。

mid 满足条件时保留 mid,因为它仍可能是最左答案;不满足时排除 mid,因为答案只能在右侧。这正是 right = midleft = mid + 1 不对称的原因。

答案二分

很多题没有给出有序数组,却能判断“某个答案是否可行”。把答案本身当作二分对象:例如速度越大越容易按时完成、船容量越大越容易装完货。

模板:最小可行答案

适用于 false → true 的谓词。right 必须是一个已知可行的上界。

/** 在闭区间 [left, right] 中找最小的可行值;假设 right 一定可行。 */
int firstTrue(int left, int right) {
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (canFinish(mid)) {
            right = mid;
        } else {
            left = mid + 1;
        }
    }
    return left;
}

LeetCode 875:爱吃香蕉的珂珂

设每小时吃 speed 根。canFinish(speed)speed 增大单调地从假变真,因此答案是最小可行速度。范围是 [1, max(piles)]

int minEatingSpeed(int[] piles, int hours) {
    int left = 1;
    int right = 1;
    for (int pile : piles) {
        right = Math.max(right, pile);
    }

    while (left < right) {
        int speed = left + (right - left) / 2;
        if (canFinish(piles, hours, speed)) {
            right = speed;
        } else {
            left = speed + 1;
        }
    }
    return left;
}

boolean canFinish(int[] piles, int hours, int speed) {
    long needed = 0;
    for (int pile : piles) {
        // 等价于 ceil(pile / speed),避免浮点数。
        needed += (pile + speed - 1) / speed;
    }
    return needed <= hours;
}

⚠️ needed 使用 long。即使每堆大小在 int 范围内,累计小时数仍可能溢出 int

模板:最大可行答案

对于 true → false 的谓词,最稳妥的写法是仍找“第一个不可行”,最后减一:

// canPlace(x) 随 x 增大从 true 变为 false;right 是第一个已知不可行值。
int lastTrue(int left, int right) {
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (canPlace(mid)) {
            left = mid + 1;
        } else {
            right = mid;
        }
    }
    return left - 1;
}

LeetCode 题型地图

题目核心谓词 / 模板关键点
704. Binary SearchlowerBound 后验证精确查找的基础
35. Search Insert PositionlowerBound返回插入位置,不存在时自然正确
34. Find First and Last PositionlowerBound + upperBound区间为 [lb, ub - 1]
69. Sqrt(x)mid * mid <= x乘法用 long 防溢出
33. Search in Rotated Sorted Array判断哪一半有序每轮至少有一半可排除
875. Koko Eating BananascanFinish(speed)最小可行答案
1011. Capacity To Ship PackagescanShip(capacity)容量越大越可行
410. Split Array Largest SumcanSplit(limit)最小化最大值
1552. Magnetic Force Between Two BallscanPlace(distance)最大可行答案

旋转数组:先识别有序半边

旋转数组不整体有序,但任意时刻至少有一半是有序的。以 LeetCode 33(元素互异)为例:

int searchRotated(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) return mid;

        if (nums[left] <= nums[mid]) { // 左半边有序
            if (nums[left] <= target && target < nums[mid]) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        } else { // 右半边有序
            if (nums[mid] < target && target <= nums[right]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
    }
    return -1;
}

常见错误与快速参考卡

错误后果修正
mid = (left + right) / 2大整数范围可能溢出left + (right - left) / 2
找左边界时写 left = midleft + 1 == right 时死循环不满足才 left = mid + 1
找右边界时直接返回相等位置重复元素时位置随机upperBound - 1
忘记检查 index == nindex < 0访问越界边界函数返回后统一验证
答案二分没有证明单调性可能得到错误答案先明确 can(x) 的真假变化方向
ceil(a / b) 用浮点数精度与类型转换风险正整数用 (a + b - 1) / b

快速参考卡

场景写法结果含义
找第一个 >= xlowerBound(x)左插入位置 / 允许相等的后继
找第一个 > xupperBound(x)右插入位置 / 严格后继
找最大 < xlowerBound(x) - 1严格前驱
找最大 <= xupperBound(x) - 1允许相等的前驱
找第一个 == xlb = lowerBound(x) 后验证第一个匹配下标
找最后一个 == xub = upperBound(x) - 1 后验证最后一个匹配下标
统计 x 的数量upperBound(x) - lowerBound(x)重复次数
最小满足条件的答案找第一个 can(x) == truefalse → true
最大满足条件的答案找第一个 can(x) == false 后减一true → false

提交前自检:搜索区间是什么?谓词是否单调?最终要第一个还是最后一个?mid 满足时是否仍可能是答案?边界返回值会不会越界?

参考资料

← 返回列表
(1 人打了分,平均分: 5.00)

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论