二分查找:从模板到边界控制

讲清闭区间、半开区间、左边界、右边界和答案二分,重点解决死循环、漏答案和返回值混乱。

已发布文章计算机基础入门8 分钟阅读发布于 2026年9月1日更新于 2026年9月15日
文章目录
  1. 它从哪里来
  2. 二分的核心思想
  3. 为什么不要写 (left + right) / 2
  4. 查找左边界
  5. 查找右边界
  6. 答案二分
  7. 常见错误
  8. 工程场景
  9. 面试题
  10. 小结
知识目录数据结构与算法:从基础到工程实践13 / 77
LOCAL NOTE高亮并记笔记
0 / 1000 · 只保存在这台设备
READER NOTES二分查找:从模板到边界控制》的本机笔记

我对“二分查找:从模板到边界控制”的理解经历过一个从会背到会用的过程。其中最明显的一点是:二分查找是我改了最多次边界的代码之一,等号放在哪里、最后返回谁,经常靠样例碰运气。后来每次看到结论,我都会追问它省掉了哪一步、又付出了什么代价,这也是这篇文章的主线。

二分查找看起来很简单:有序数组里,每次看中间,比目标小就往右找,比目标大就往左找。但它也是最容易写出边界 bug 的算法之一。

二分真正难的地方不是 mid,而是你有没有想清楚:当前搜索区间代表什么,哪些位置已经被排除了,最后返回哪个边界。

它从哪里来

二分查找的思想来自有序信息中的逐步排除,但早期程序实现长期容易出现边界错误。它看似只有几行,却同时涉及区间定义、中点计算、循环不变量和重复元素边界,因此成为“简单思想如何写成严格程序”的经典例子。

二分的核心思想

二分查找边界移动

图:二分每一步都在缩小可能答案区间

二分适合的问题有两个共同点:

  1. 答案空间有顺序。
  2. 可以判断某个位置或答案是否满足条件,并据此排除一边。

最典型的是有序数组查找:

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;
}

这里使用的是闭区间 [left, right]。循环条件是 left <= right,说明区间为空的时刻是 left > right

为什么不要写 (left + right) / 2

更稳的写法是:

int mid = left + (right - left) / 2;

原因是 left + right 在极大数组下可能整数溢出。虽然普通业务里不一定遇到,但这是一个很好的习惯:算法代码要对边界敏感。

查找左边界

很多题不是问“有没有”,而是问“第一个等于 target 的位置”。

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

这里使用左闭右开区间 [left, right)。它返回的是第一个 >= target 的位置。如果返回位置还在数组内,并且 nums[pos] == target,它就是左边界。

int firstEqual(int[] nums, int target) {
    int pos = lowerBound(nums, target);
    return pos < nums.length && nums[pos] == target ? pos : -1;
}

这个模板很实用,因为它也能解决“插入位置”问题。

查找右边界

右边界可以转换成“第一个大于 target 的位置再减一”。

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

最后一个等于 target 的位置是:

int lastEqual(int[] nums, int target) {
    int pos = upperBound(nums, target) - 1;
    return pos >= 0 && nums[pos] == target ? pos : -1;
}

我的建议是:边界题尽量用 lowerBound/upperBound 思维,不要在普通查找模板里硬改,否则很容易出现死循环或漏答案。

答案二分

二分不只能查数组,也能查“答案”。

比如给定若干包裹重量和天数,求船的最小运载能力。运力越大,越容易在规定天数内运完;运力越小,越难。这就是单调性。

int minCapacity(int[] weights, int days) {
    int left = 0, right = 0;
    for (int w : weights) {
        left = Math.max(left, w);
        right += w;
    }
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (canShip(weights, days, mid)) {
            right = mid;
        } else {
            left = mid + 1;
        }
    }
    return left;
}

boolean canShip(int[] weights, int days, int cap) {
    int used = 1, load = 0;
    for (int w : weights) {
        if (load + w > cap) {
            used++;
            load = 0;
        }
        load += w;
    }
    return used <= days;
}

这类题最重要的是找到判断函数 can(...),并确认它是单调的。

常见错误

  • 区间定义混乱:一会儿闭区间,一会儿半开区间。
  • left = midright = mid 导致不收敛。
  • 忘记空数组。
  • 返回 left 前没有检查是否真的命中。
  • 答案二分时上下界设错。
  • 判断函数方向写反。

工程场景

二分在工程里也很常见:

  • 日志中按时间戳查找第一条大于某时间的记录。
  • 配置版本中查找生效版本。
  • 数据库分页或索引定位的思想。
  • 压测中查找系统最大可承载 QPS。
  • 分布式系统里查找故障出现的第一个版本。

二分的价值不是只会写数组查找,而是建立“单调性 + 排除一半”的思维。

面试题

  1. while(left <= right)while(left < right) 有什么区别?
  2. 为什么 lowerBound 返回的是第一个 >= target 的位置?
  3. 二分为什么可能死循环?如何避免?
  4. 什么样的问题可以做答案二分?
  5. 如果数组有重复元素,如何找第一个和最后一个目标值?

小结

二分的灵魂是区间不变量。只要你能说清楚区间含义、排除逻辑和最终返回值,二分就不会再靠运气。后面很多高级题,本质上只是把数组位置换成了答案空间。

JARVIS · 当前文章

有哪里没看懂?可以只问这篇。

Jarvis 会限定在《二分查找:从模板到边界控制》及其公开关联内容中检索,并把引用定位回原文章节。

JARVIS / ARTICLE针对《二分查找:从模板到边界控制》提问
当前范围二分查找:从模板到边界控制不会悄悄扩大到全站
0 / 1000

准备好了。当前只会围绕二分查找:从模板到边界控制回答。

READER SIGNAL

这篇内容对你有帮助吗?

不需要登录。你的反馈会直接进入作者待处理列表。