Leetcode二分查找(4)
35. 搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。请必须使用时间复杂度为 O(log n) 的算法。
解题思路总览
- 标准二分(lower_bound 半开区间写法)
- 标准二分(闭区间写法)
- 递归二分
- 利用 Java Arrays.binarySearch 封装处理
- 变体:位运算取中 / 防溢出与模板细节
- 拓展:未知长度数组的指数扩展 + 二分(非本题必需,思路扩展)
题目回顾:给定升序(非递减)整数数组 nums 与 target,若存在返回其索引,否则返回按顺序插入后仍保持有序的索引(即第一个 >= target 的位置)。要求 O(log n)。这就是典型的 lower_bound 问题。
思路一:标准二分(lower_bound 半开区间写法)
原理与适用场景:
采用半开区间 [l, r) 表示当前搜索范围,维持循环不变量:目标插入位置一定在 [l, r) 中。中点 mid = l + (r - l)/2。若 nums[mid] >= target,将可能的插入位置右边界收缩到 mid;否则抛弃左半包含 mid 的部分,令 l = mid + 1。最终 l == r 即为第一个 >= target 的位置:
- 若该位置在数组长度内且值等于 target,则它就是目标索引。
- 否则该位置就是插入点。
半开区间避免处理 mid - 1 / mid + 1 时越界边界判断,模板简洁统一,可复用于各类 lower_bound / upper_bound 问题。
实现步骤:
- 令 l = 0, r = n(注意 r 为 n,而不是 n-1)。
- while(l < r):计算 mid。
- 若 nums[mid] >= target:r = mid;否则 l = mid + 1。
- 循环结束返回 l。
JAVA 代码实现:
public class SolutionLowerBound {// 返回 target 的索引或其应插入位置(第一个 >= target 的下标)public int searchInsert(int[] nums, int target) {int n = nums.length; // 数组长度int l = 0; // 左边界(包含)int r = n; // 右边界(不包含)while (l < r) { // 区间非空int mid = l + (r - l) / 2; // 取中,防溢出if (nums[mid] >= target) { // mid 位置可能是第一个 >= targetr = mid; // 收缩右端到 mid} else { // nums[mid] < targetl = mid + 1; // 插入点一定在 mid 右侧}}return l; // l == r 为第一个 >= target 的位置(可能等于 n)}
}
思路二:标准二分(闭区间写法)
原理与适用场景:
使用闭区间 [l, r]。循环条件 l <= r。维护答案变量 ans,记录第一个 >= target 的候选位置。每当 nums[mid] >= target,用 ans = mid 保存并 r = mid - 1 尝试更左;否则 l = mid + 1。最终若 ans 未被更新(保持初值 n)则插入位置在数组末尾。闭区间写法适合对传统二分 while(l <= r) 更熟悉的人。
实现步骤:
- ans = n(默认插入到末尾)。
- while(l <= r) 取 mid。
- 若 nums[mid] >= target:更新 ans,r = mid - 1。
- 否则 l = mid + 1。
- 返回 ans。
JAVA 代码实现:
public class SolutionClosedInterval {public int searchInsert(int[] nums, int target) {int n = nums.length; // 长度int l = 0; // 左边界int r = n - 1; // 右边界int ans = n; // 默认插入到末尾while (l <= r) { // 闭区间还存在元素int mid = l + (r - l) / 2;// 中点if (nums[mid] >= target) {// mid 可能是答案ans = mid; // 记录候选r = mid - 1; // 尝试找更左} else { // nums[mid] < targetl = mid + 1; // 去右区间}}return ans; // 第一个 >= target 的位置}
}
思路三:递归二分
原理与适用场景:
递归在区间 [l, r] 中寻找第一个 >= target 的位置。若 l > r 返回 n(表示未找到更小位置)。取 mid:
- 若 nums[mid] >= target:答案在左半或 mid,自身与左半递归返回值取最小。
- 否则在右半。
递归深度 O(log n)。适合希望把二分统一为递归模板的场景。
实现步骤:
- 递归函数返回第一个 >= target 的索引或 n。
- 初始调用 dfs(nums, 0, n-1, target, n)。
- 返回结果。
JAVA 代码实现:
public class SolutionRecursiveLB {public int searchInsert(int[] nums, int target) {return dfs(nums, 0, nums.length - 1, target, nums.length);}private int dfs(int[] a, int l, int r, int target, int n) {if (l > r) { // 区间空,返回 n 代表插入末尾return n;}int mid = l + (r - l) / 2;// 取中if (a[mid] >= target) { // mid 可能为答案,继续探索左侧int left = dfs(a, l, mid - 1, target, n);return Math.min(mid, left); // 取更左的} else { // a[mid] < target,只能右侧return dfs(a, mid + 1, r, target, n);}}
}
思路四:利用 Java Arrays.binarySearch 封装处理
原理与适用场景:
Java 标准库 Arrays.binarySearch(nums, target) 返回:
- 若找到:返回 >=0 的索引。
- 若未找到:返回 (-(插入点) - 1)。插入点即第一个 > target 的下标,也即第一个 >= target(因为未找到等于)。
因此直接解码即可得到答案。适合快速实现,降低样板代码量,但在某些面试中展示手写二分更能体现功底。
实现步骤:
- 调用 idx = Arrays.binarySearch(nums, target)。
- 若 idx >= 0 返回 idx。
- 否则插入点 = -idx - 1。
- 返回插入点。
JAVA 代码实现:
import java.util.Arrays; // 导入工具类public class SolutionLibrary {public int searchInsert(int[] nums, int target) {int idx = Arrays.binarySearch(nums, target); // 标准库二分if (idx >= 0) { // 找到直接返回return idx;}// 未找到,idx = -(insertionPoint) - 1return -idx - 1; // 解码得到插入点}
}
思路五:位运算取中 / 模板细节(属于写法优化,不单独改变复杂度)
原理与适用场景:
在极端性能场景可用 mid = (l + r) >>> 1(无符号右移)替代 l + (r - l)/2;在 Java 中 int 溢出时 l + r 可能为负导致结果错误,因此推荐使用 l + (r - l)/2 或 (l + r) >>> 1(当 l,r >=0 时安全)。属于实现细节优化,可结合思路一或二使用。
实现步骤:
- 在二分循环中用 mid = (l + r) >>> 1。
- 其余逻辑不变。
(示例略,因核心逻辑同思路一。)
思路六:拓展 - 未知长度数组 / 流式数据(指数扩展 + 二分)
原理与适用场景:
若无法直接获得数组长度(例如某些 API 仅支持 get(i) 且越界抛异常),可先指数扩展边界:
- 令 r = 1,不断检查 nums[r] 与 target 比较,若 nums[r] < target 则 r *= 2。
- 找到第一个 nums[r] >= target 或越界时停止,此时目标插入点一定在 (r/2, r] 内。
- 在这个区间执行常规二分。指数扩展步数 O(log pos),整体仍 O(log n)。
本题已给定长度,不必使用,只作思维拓展。
实现步骤(高层):
- 边界扩展。
- 二分搜索 lower_bound。
- 返回结果。
补充说明(对比分析)
- 正确满足 O(log n) 的实现:思路一、二、三、四、六(六为扩展情景)。
- 复杂度:
- 思路一/二/三/四:时间 O(log n),空间 O(1)(递归 O(log n))。
- 思路六:时间 O(log n),空间 O(1)(若递归则 O(log n))。
- 推荐优先级:
- 首选思路一(半开区间 lower_bound 模板)。
- 需要展示传统写法可选思路二。
- 递归视个人风格;库函数写法快速但展示度较低。
- 边界与健壮性:
- 空数组:半开区间写法自动返回 0。
- target 小于最小值 -> 返回 0。
- target 大于最大值 -> 返回 n。
- 全部元素相同:lower_bound 仍正确给出 0 或 n(若 target 更大)。
- 常见错误:
- 闭区间写法退出条件混乱导致死循环(如用 while(l < r) 但更新 r = mid - 1)。
- mid = (l + r)/2 可能溢出(极大数据时)。
- 返回 r 或 l 搞错:需基于循环不变量严格推导。
- 测试用例建议:
- nums=[1,3,5,6], target=5 -> 2
- nums=[1,3,5,6], target=2 -> 1
- nums=[1,3,5,6], target=7 -> 4
- nums=[1,3,5,6], target=0 -> 0
- nums=[], target=5 -> 0
- nums=[1], target=1 -> 0
- nums=[1], target=2 -> 1
- nums=[1,1,1], target=1 -> 0
- 总结:本题本质是 lower_bound,掌握一套稳健模板(思路一)即可在同类区间定位、统计频次、插入位置等问题上快速迁移。
综上,采用半开区间 lower_bound 模板实现最简洁、低错率、可直接推广,是最推荐解法。