左边界是第一个≥target的位置,需在nums[mid]==target时令r=mid−1继续左搜;右边界是最后一个≤target的位置,等价于upper_bound返回值减1;二者均推荐闭区间[l,r]实现,且应独立两次二分。

二分查找左边界:lower_bound 的手写逻辑
左边界指第一个 ≥ target 的位置,标准库 std::lower_bound 返回的就是这个,但手写时容易错在循环条件和边界更新上。
关键点是:当 nums[mid] == target 时,不能直接返回,要继续往左搜(r = mid - 1),因为目标是“最左边那个”。
常见错误现象:return mid 或 l = mid + 1 导致跳过左边界;用 l 却没处理最终 <code>l > r 时的返回值。
- 推荐使用闭区间
[l, r],初始l = 0, r = nums.size() - 1 - 循环条件必须是
l ,退出时 <code>l恰好是左边界位置(即使越界) - 每次
mid = l + (r - l) / 2,避免整型溢出 - 若
nums[mid] >= target,则r = mid - 1;否则l = mid + 1 - 最后检查
l是否在[0, nums.size())内且nums[l] == target,否则说明不存在
二分查找右边界:upper_bound 的手写逻辑
右边界指最后一个 ≤ target 的位置,等价于标准库 std::upper_bound 返回位置减一。它不是“第一个 > target”,而是“最后一个 == target”——这点常被混淆。
立即学习“C++免费学习笔记(深入)”;
手写时最易错的是把右边界当成 lower_bound 的变体而只改一个符号,结果逻辑断裂。
- 仍用闭区间
[l, r],但判断分支相反:当nums[mid] 时,<code>l = mid + 1(向右收缩) - 循环退出后,
r就是右边界位置(因为每次满足条件都把l推过去,r停在最后一个合法位置) - 必须验证
r >= 0 && nums[r] == target,否则 target 不存在或全小于它 - 注意:
upper_bound标准行为是返回第一个 > target 的位置,所以手写右边界 =std::upper_bound(...) - 1
左右边界合并实现:一次二分还是两次?
两次独立二分(先找左、再找右)更清晰、不易出错,时间复杂度仍是 O(log n),实际性能损失可忽略。
试图“一次遍历同时维护左右”看似节省,但逻辑膨胀、分支爆炸,调试成本远高于多一次 log n 查找。
- 不要共享
l/r变量,左右边界搜索的收缩方向完全相反 - 如果数组极小(比如
n ),线性扫描反而更快,二分优势在 <code>n > 100以上才稳定 - 对已知 target 一定存在的情况,可省略边界有效性检查,但生产代码建议保留
- C++20 起可用
std::ranges::lower_bound和std::ranges::upper_bound,支持自定义迭代器和谓词,更安全
边界查找的典型误用场景
最常踩坑的是把左/右边界函数直接用于插入位置计算,却忘了它们返回的是索引而非偏移量。
例如:想在有序数组中插入 target 并保持有序,用 lower_bound 得到的 pos 就是插入位置;但若误用 upper_bound,会导致重复元素插在错误一侧。
- 插入不重复元素:用
lower_bound - 插入并允许重复:用
upper_bound可让新元素排在所有相同元素之后 - 统计 target 出现次数:先算
left = lower_bound,再算right = upper_bound,结果为right - left - 当数组为空或全小于/大于 target 时,
lower_bound返回nums.size(),upper_bound返回 0 —— 这些边界值必须显式处理,不能假设一定落在[0, n)内
左边界和右边界的核心差异不在代码长度,而在“收缩方向”与“终止状态”的绑定关系。写错一次,往往是因为没想清楚:我到底是在找“第一个满足条件的位置”,还是“最后一个满足条件的位置”——这个判断比 while 里写 l <= r 还重要。


















