目录
引言
在工程实践中,二分查找通常不是为了解决“查找某个值”,而是回答以下问题之一:
最大能到多少?
系统最多能承受多少并发?最大安全 QPS 是多少?
最小该设多少?
超时时间至少要多少?限流阈值最低是多少才不误伤?
从哪一步开始系统变坏?
哪个版本开始出现异常?哪个配置开始触发 SLA 违约?
只要你的问题满足“参数变化 → 系统表现单调变化”,就可以考虑二分查找。
二分查找,本质上是用极少的探测成本,快速定位系统的临界边界。
基础概念
二分查找是一种基于 有序性(单调性) 的搜索算法:
每一步都将搜索区间缩小一半
时间复杂度:O(log n)
空间复杂度:O(1)(迭代实现)
核心前提只有一个:
搜索空间必须满足单调性
单调性 ≠ 排序
这是工程中最常见的误区。
| 场景 | 是否能二分 |
| 排好序的数组 | 是 |
| 时间 → QPS 是否超过阈值 | 是 |
| 并发数 → 延迟是否超标 | 是 |
| 配置版本号 | 是 |
| 任意无序集合 | 否 |
二分查找的本质不是“查数据”,而是在一个单调变化的系统参数空间中,快速定位边界值。
基本思想
抽象成三个要素
搜索区间:[left, right]
判定函数:check(mid)
区间收缩规则
本质逻辑
如果 mid 满足条件:
丢弃一半不可能的区间
否则:
丢弃另一半
这是一种基于判定的贪心式缩减,但它不是贪心算法,而是:
基于单调性的确定性缩小。
工程二分通用模板
/**
* 工程二分查找通用模板
* 搜索的是「满足条件的最大值 / 最小值」
*/
int binarySearch(int left, int right) {
while (left < right) {
// 防溢出计算中点
int mid = left + (right - left) / 2;
if (check(mid)) {
// mid 可行,答案在 [mid, right] 或更远
left = mid + 1; // 或 left = mid(取决于语义)
} else {
// mid 不可行,答案在 [left, mid - 1]
right = mid - 1;
}
}
return left;
}
工程中最常见的三类二分目标:
找「最大可行值」
check(x) = 是否可行
→ 常用于容量、并发、QPS
找「最小不可行值」
check(x) = 是否失败
→ 常用于熔断、阈值
找「第一个满足条件的位置」
check(x) = 是否满足条件
→ 常用于版本、时间点
写代码前,先用一句话说明你要找的是哪一种边界。
误区警告
单调性判断错误(最致命)
“我感觉它是单调的”≠“它在生产环境一定单调”
工程中常见的非严格单调情况:
自适应线程池
JIT预热
延迟在某些区间反而下降
GC、缓存命中导致非严格单调
工程建议:
只对“趋势单调”使用
判定函数允许少量噪声
通过多次采样减少波动影响
工程后果:
二分仍然会“正常收敛”,但结果是稳定且错误的,这是最危险的一类 bug。
判定函数成本过高
O(log n) × check(mid)
如果 check() 很慢,整体仍然很慢。
工程建议:
判定函数要可缓存(缓存中间结果)
优先使用近似指标(P95 / 滑动窗口)
采用异步评估,并行执行多个判定
在工程中,check(mid) 往往比二分本身更昂贵,二分只是“外壳”,真正的性能瓶颈在判定逻辑。
二分边界语义不清
常见混乱:
找第一个true?
找最后一个true?
找最大可行值?
工程建议:
写代码前先用一句话说明你要找的是什么边界。例如:"找最后一个满足SLA的并发数"或"找第一个导致超时的版本"
工程实践
配置 / 参数探测
最大线程数
最大并发连接数
最优批量大小
线程数 ↑ → 系统延迟 ↑(单调)
案例:最大并发线程数优化
在不超过 200ms 延迟的前提下,系统最多支持多少并发线程?
public class ConcurrencyTuner {
public int findMaxConcurrency(int low, int high) {
while (low < high) {
// 向上取整,防止死循环
int mid = low + (high - low + 1) / 2;
if (check(mid)) {
low = mid; // mid 可行,向右探索
} else {
high = mid - 1; // mid 不可行,缩小右边界
}
}
return low;
}
// 判定函数:是否满足 SLA
private boolean check(int concurrency) {
long latency = simulateLatency(concurrency);
return latency <= 200;
}
private long simulateLatency(int concurrency) {
// 示例:真实工程中可能来自压测 / 监控 / 模拟
return 50 + concurrency * 3;
}
}
在真实工程中,check(mid) 通常来自:
压测结果(本地 / CI)
监控系统(Prometheus / SkyWalking)
线上探测(金丝雀流量)
例如:
private boolean check(int concurrency) {
// 示例:来自监控系统的 P99 延迟
long p99Latency = metricsClient.queryP99Latency(concurrency);
return p99Latency <= 200;
}
限流与熔断阈值计算
QPS 上限
限流窗口大小
超时时间探测
QPS ↑ → 错误率 ↑(单调)
案例:通过二分查找确定系统能承受的最大健康QPS
public class RateLimitCalculator {
public int findMaxQps(int min, int max) {
while (min < max) {
int mid = min + (max - min + 1) / 2;
if (isHealthy(mid)) {
min = mid;
} else {
max = mid - 1;
}
}
return min;
}
private boolean isHealthy(int qps) {
double errorRate = mockErrorRate(qps);
return errorRate < 0.01;
}
private double mockErrorRate(int qps) {
return qps <= 500 ? 0.005 : 0.02;
}
}
索引与存储系统
B+Tree 节点内查找
SSTable 查找
日志 offset 定位
案例:有序 offset 定位
public int findOffset(long[] offsets, long target) {
int left = 0, right = offsets.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (offsets[mid] == target) {
return mid;
} else if (offsets[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
在存储系统中,二分查找之所以被广泛使用,并不是因为它“最优”,而是因为它在性能可预测性、实现复杂度、稳定性之间达到了最佳平衡。
灰度发布 / 回滚边界
最后一个稳定版本
第一个异常版本
版本号 ↑ → 是否异常(单调)
案例:定位首个异常版本
public class GrayReleaseChecker {
public int firstBadVersion(int left, int right) {
while (left < right) {
int mid = left + (right - left) / 2;
if (isBad(mid)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
private boolean isBad(int version) {
return version >= 42;
}
}
灰度发布中的二分,本质是用最少的线上探测次数,定位系统开始失稳的边界版本。
结语
二分查找是一种工程级“试探策略”。
在工程系统中,二分查找解决的从来不是“能不能找到某个值”,而是在不确定、昂贵、甚至带风险的系统探测中,如何用最少的尝试次数,逼近真实边界。
它背后的价值不在于 O(log n) 的时间复杂度,而在于:
探测成本可控
收敛路径确定
行为结果可解释
当你开始把二分查找用于:
并发与容量上限评估
限流与熔断阈值确定
灰度发布与回滚边界定位
你实际上已经在做一件事:
把“拍脑袋的参数调整”,变成“有边界、有依据的工程决策”。
真正的工程能力提升,不在于你是否会写二分查找,
而在于你是否能清楚地回答三个问题:
我假设的单调性是否真实存在?
我的判定函数是否代表系统的核心健康指标?
我现在找的,到底是哪一个“不可再越过”的边界?
当你开始用这种方式思考问题时,二分查找就不再是算法题,而是一种工程系统中极其可靠的决策工具。

被折叠的 条评论
为什么被折叠?



