先记住这个答案
二分查找的两种区间写法对应不同循环不变量。左闭右闭[L,R]中,搜索包含两端,比较后必须排除mid,故更新为L=mid+1或R=mid-1,循环继续条件是L<=R,结束时L=R+1。左闭右开[L,R)中,右边界不包含候选,若mid不满足条件则L=mid+1,否则R=mid,循环条件是L<R,结束时L==R,该位置正好是第一个满足条件的下标。实现查找具体值常用闭区间,寻找下界或插入位置则开区间更自然。
- 闭区间退出时L=R+1,开区间退出时L=R,这是关键区别
- 开区间适合求第一个满足条件的位置,闭区间适合查精确值
- 每次更新必须维持不变量,否则会死循环或漏解
两种区间的收缩规则
左闭右闭中,初始L=0,R=n-1,区间[L,R]内每个下标都可能是答案。每次取mid,若arr[mid]不满足条件,则必须让区间收缩到不包括mid的一侧,于是L=mid+1或R=mid-1。因为只要还有未检查的元素就应继续,循环条件写成while(L<=R),退出时L=R+1,代表区间彻底被排除。
左闭右开中,L指向第一个可能的答案,R指向边界外的哨兵,区间[L,R)始终保持arr[L-1]<target且arr[R]>=target(若存在)。当arr[mid]>=target时,mid可能是答案,收缩右侧为R=mid;否则L=mid+1排除mid。循环条件是L<R,退出时L==R,正好是第一个不满足/满足的分界点。
查找插入位置时的开区间优势
给定升序数组[1,3,3,5,7],需要确定目标值2应该插入的下标,即找到第一个>=2的位置。约束是数组有序且可能重复,要求返回下标。若用左闭右开,直接L=0,R=n,循环结束后返回L即可,无需额外变量,代码天然处理元素全部小于目标时返回n。
左闭右闭同样可以直接返回left作为插入位置,但需要理解当数组全小于target时left会等于数组长度,使用前需验证;开区间写法把终点条件嵌入区间定义,错误率更低。
function lowerBoundClosed(arr, target) {
let left = 0, right = arr.length - 1;
while (left <= right) {
const mid = left + ((right - left) >> 1);
if (arr[mid] >= target) right = mid - 1;
else left = mid + 1;
}
return left;
}
function lowerBoundOpen(arr, target) {
let left = 0, right = arr.length;
while (left < right) {
const mid = left + ((right - left) >> 1);
if (arr[mid] >= target) right = mid;
else left = mid + 1;
}
return left;
}
const cases = [
{ arr: [1, 3, 3, 5], target: 3 },
{ arr: [1, 3, 3, 5], target: 2 },
{ arr: [1, 3, 3, 5], target: 6 },
{ arr: [], target: 1 },
];
for (const { arr, target } of cases) {
console.log(`arr=${JSON.stringify(arr)} target=${target} -> closed:${lowerBoundClosed(arr, target)}, open:${lowerBoundOpen(arr, target)}`);
}查看输出与解释
arr=[1,3,3,5] target=3 -> closed:1, open:1
arr=[1,3,3,5] target=2 -> closed:1, open:1
arr=[1,3,3,5] target=6 -> closed:4, open:4
arr=[] target=1 -> closed:0, open:0两种写法都对[1,3,3,5]返回第一个不小于目标值的下标:目标3返回1(第一个3),目标2返回1,目标6返回数组长度4;空数组返回0。闭区间通过right=mid-1收缩,开区间通过right=mid保留候选。时间复杂度均为O(log n),空间O(1)。该代码直接运行在Node.js或浏览器控制台。
边界容易失效的坑
左闭右开若误写成while(L<=R),且更新仍用R=mid,当L=R时循环继续,mid等于边界,R=mid不改变,导致无限循环。同样,闭区间如果用L=mid代替mid+1,在mid不等于目标时区间不缩小,也会死循环。判断的关键是检查每次迭代区间长度是否严格递减。
另一个失效场景是目标数组为空。闭区间写法需保证R=n-1,空数组时R=-1,循环不执行,此时若返回L=0是正确插入位置,但若直接访问arr[mid]会越界。开区间初始R=0,同样不执行循环,返回0。处理时留意先判断数组长度或让R从n开始,避免对哨兵取值。
容易答错的地方
- 认为开区间只是一种风格
- 其实两者维护的语义完全不同,开区间天然对应二分查找的谓词版,闭区间适合精确查找。混用会导致区间收缩不对,比如在开区间里用
R=mid-1会跳过答案。依据是循环不变量必须与更新操作一致。 - 以为闭区间返回`left`总是正确
- 闭区间写法和开区间写法最后的
left值相同,都指向第一个不小于目标的位置;但数组全小于target时left会越出数组长度,此时用arr[left]会越界。若要找某个确切值,必须额外判断该位置是否存在。
面试官还会怎么问?
左闭右开写`right = mid`时,为什么不会死循环?
因为当mid<right时区间长度至少为1,若right=left+1则mid=left,此时若arr[left]>=target则right=mid即right=left,循环结束;否则left=mid+1使得区间缩小。每次迭代mid取自下中位数,且收缩方向明确排除一个元素。
查找具体值而不是下界时,两种写法哪个更直接?
查找精确相等用闭区间方便,遇到arr[mid]==target直接返回mid。开区间写法需要先求下界再比较arr[pos]是否等于target,多一步检查,但边界仍一致。工程上求存在性多用闭区间,求范围用开区间。
二分答案时通常用开区间还是闭区间?
二分答案往往找最小可行解或最大可行解,此时用开区间配合单调谓词最自然,如[L,R)表示可行域分界,最终返回R。闭区间需要维护答案变量,两者等价但开区间更少出错。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。