算法困惑问答 · LC 704 二分查找
二分查找为什么会死循环?边界到底怎么写才对?
直接答案
死循环只有一个病根:某一轮循环结束后,区间没有变小。最典型的写法是收缩时用 l = mid(不加一)——当区间只剩两个元素时,mid 向下取整恒等于 l,这一轮跑完 l 还在原地,永远转圈。解法是让每次收缩都实打实排除 mid:闭区间写法里 mid 已经比较过、确定不是答案,就该 l = mid + 1 或 r = mid − 1。
复现一次死循环,病根一目了然
设 l=3、r=4,mid = (3+4)/2 = 3。若代码写的是「target 更大时 l = mid」,这一轮结束后 l 仍是 3、r 仍是 4——状态和上一轮完全相同,程序在这两个数之间永远打转。只要收缩分支里有一个「可能不动」的分支,死循环就埋下了。
自查方法也由此而来:拿两个元素的区间手推一轮,检查每个分支是否都让 l 或 r 真的移动了。这个两元素测试能抓住绝大多数二分 bug。
一套能坚持到底的写法
推荐闭区间模板:l = 0,r = n − 1,while (l <= r),比较后 l = mid + 1 或 r = mid − 1。它的循环不变量是「答案若存在,必在 [l, r] 内」;mid 比较过就被排除,区间每轮至少缩 1,必然终止。while 用 <= 是因为 [l, r] 是闭区间,l == r 时还有一个候选没检查,漏判它会错过最后一个元素。
找边界(第一个 ≥ target 的位置)时命中不返回:记下 mid 作为候选,继续 r = mid − 1 往左压,循环结束时候选即左边界。开区间写法(while (l < r) 配 r = mid)同样自洽,但开闭区间和收缩方式必须配套,两种模板混用才是大多数「玄学 bug」的来源。
顺带一个工程细节:mid 写成 l + (r − l) / 2 而不是 (l + r) / 2——后者在 l、r 都很大时 l + r 可能溢出 int 上限,先减后加永不溢出。
代码关键行(Java)
int l = 0, r = n - 1;
while (l <= r) { // 闭区间配 <=
int mid = l + (r - l) / 2; // 防溢出
if (nums[mid] == target) return mid;
if (nums[mid] < target) l = mid + 1; // 排除 mid
else r = mid - 1; // 排除 mid
}
return -1;两个收缩分支都带 ±1,mid 被实打实排除,区间每轮必缩——这就是不死循环的全部保证。改成 l = mid 或 r = mid 的任何一支,都要换成与之配套的开区间模板。
常见追问
while (l < r) 和 while (l <= r) 到底哪个对?
都对,关键在配套:闭区间 [l, r] 用 l <= r 配 mid±1;左闭右开 [l, r) 用 l < r 配 r = mid。错的从来不是某个条件,而是混搭。选一套写熟,别来回换。
找「第一个」「最后一个」时怎么防止跳过答案?
命中时不返回、只收缩:找第一个就记录 mid 后 r = mid − 1 继续向左,找最后一个就 l = mid + 1 继续向右。因为答案已被记录,mid 照样可以排除,区间照样必缩。
把这道题彻底吃透
LC 704「二分查找」有逐步交互动画和完整图文题解,配着本页的结论看,一遍就通。