先记住这个答案
使用左闭右开写法,令 left=0, right=n,循环不变量是 left 左侧元素均小于 target,right 右侧元素均不小于 target。每次比较中位数,若 nums[mid] < target 则 left=mid+1,否则 right=mid。终止于 left==right,该位置就是下界,插入 target 后保持有序。目标存在时也返回第一个相等索引,目标不存在时返回正确插入位。
- 插入位置是第一个不小于目标的索引
- 左闭右开写法免去存在性判断分支
- 循环结束的 left 即 lower_bound 结果
为何循环结束时 left 落在插入点
二分查找每次依据与中位数的比较收缩候选区间。采用左闭右开区间 [left, right) 表示包含下界在内的区间,不变量是 left 左端元素全小于 target,right 右端元素全不小于 target。收缩区间时这一性质被保留。
循环条件 left < right 持续,取中点 mid。若 nums[mid] < target,则 mid 及其左边都小于目标,可令 left=mid+1 同时不破坏不变量;反之 right=mid 排除右侧不含下界的部分。最终 left 与 right 相遇,即第一个使得谓词不小于 target 成立的位置,这正是插入点。
function searchInsert(nums, target) {
let left = 0, right = nums.length;
while (left < right) {
const mid = left + Math.floor((right - left) / 2);
if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
console.log(searchInsert([1,3,5,6],5)); // 2
console.log(searchInsert([1,3,5,6],2)); // 1
console.log(searchInsert([1,3,5,6],7)); // 4
console.log(searchInsert([1,3,5,6],0)); // 0
console.log(searchInsert([],5)); // 0查看输出与解释
2
1
4
0
0时间复杂度 O(log n),空间 O(1)。输入包含命中、未命中、大于所有、小于所有、空数组五类,返回值均正确代表插入后保持升序的索引。
应用:LeetCode 35 搜索插入位置
题目要求给定升序数组与目标值,返回目标值存在时的下标或应插入的位置。例如 nums=[1,3,5,6],target=2 返回 1,target=7 返回 4。若用朴素二分找到目标才停止,需额外补判断;而用 lower_bound 直接得结果,代码简洁统一。
该解法等价于标准库的 lower_bound。Java 的 Arrays.binarySearch 返回负数时可用 "-(result)-1" 的公式推导插入点,但自己实现用此模式更直观。面对重复值它返回最左侧匹配位置,将新元素插在此处仍保持有序,满足大多数场景的稳定性需求。
边界条件与失效风险
数组为空时,循环不执行,left=0 返回 0,正确。目标小于首元素时,每次比较都落入 else 分支,right 持续缩小到 0,返回 0;目标大于所有元素时,每次触发 left=mid+1,最终 left=n 返回数组长度,即追加到末尾。这两类无需特判。
若写成左闭右闭解法,需控制 right 为 n-1,结束时可能返回 left 也要检查越界,容易出错。左闭右开形成 right=n 则避免该问题。当含重复值且要求插入到最后一个相等元素之后,应改用 upper_bound(第一个大于目标的位置),否则 lower_bound 插在相等项之前。
容易答错的地方
- 失败时返回 mid 即可
- 这是常见错误:二分查找找不到时 mid 可能落在任意位置,只有维护下界不变量才能保证 left 是精确插入点。直接返回 mid 往往结果错误,例如 nums=[1,3], target=2 时 mid=0,但正确插入点是 1。
- 循环结束后需要检查 left 是否越界
- 使用左闭右开区间时 left 范围本就是 0 到 n,返回它无需额外检查。很多左闭右闭教程结束后需要判断 left==n 等,但左闭右开写法将越界情况自然映射为返回值,省去分支。
面试官还会怎么问?
如果目标存在且重复,应该返回哪个索引作为插入点?
若需把新值插入到相等项之前,用 lower_bound 返回第一次出现的位置;若插入到相等项之后,用 upper_bound 返回第一个大于目标的索引。两种结果都不破坏升序,决策取决于业务上的稳定性需求。
如何用二分查找同时判断目标是否存在并返回正确插入位?
先调用 lower_bound 得到 pos,再比较 pos 是否小于数组长度且 nums[pos] == target。若相等则存在,否则不存在且 pos 就是插入位置。这样只花费一次 O(log n) 查询,无需额外状态。
计算中点时用 left + Math.floor((right-left)/2) 有什么好处?
避免 left+right 直接相加可能引起的整数溢出,在其他强类型语言中尤其关键。同时它使 mid 偏向左侧,配合 while(left<right) 可防止区间无变化导致死循环。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。