力扣解题-35. 搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
提示:
1 <= nums.length <= 10⁴
-10⁴ <= nums[i] <= 10⁴
nums 为 无重复元素 的 升序 排列数组
-10⁴ <= target <= 10⁴
Related Topics
数组、二分查找
第一次解答
解题思路
核心方法:二分查找边界定位法,利用升序数组的特性,通过二分查找缩小搜索范围,最终left指针的位置即为目标值的索引(存在时)或插入位置(不存在时),时间复杂度严格为O(logn)、空间复杂度O(1),完全满足题目要求且是最优解法。
核心逻辑拆解
二分查找找插入位置的核心规律:
- 二分查找的核心目标:在升序数组中,找到第一个大于等于目标值的元素位置(该位置就是插入位置);
- 指针移动规则:
- 若中间值
<目标值:说明目标值在右半区间,将左指针移到mid+1; - 若中间值
≥目标值:说明目标值在左半区间(或就是当前值),将右指针移到mid-1;
- 若中间值
- 循环终止条件:
left > right,此时left恰好指向“第一个大于等于目标值的位置”,也就是插入位置。
具体执行逻辑
- 初始化指针:
left:左边界,初始为0;right:右边界,初始为nums.length-1(数组最后一个元素的索引);
- 二分循环:当
left ≤ right时持续循环:- 计算中间索引
mid = left + (right-left)/2(避免(left+right)/2的整数溢出); - 获取中间值
midVal = nums[mid]; - 比较
midVal与target:- 若
midVal < target:目标值在右半区,更新left = mid + 1; - 否则:目标值在左半区(或就是当前值),更新
right = mid - 1;
- 若
- 计算中间索引
- 返回结果:循环结束后,
left即为目标值的索引(存在)或插入位置(不存在)。
执行流程可视化(以示例2 nums=[1,3,5,6]、target=2为例)
| 循环次数 | left | right | mid | midVal | 比较结果 | 指针更新 | 说明 |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 3 | 1 | 3 | 3≥2 | right=0 | 目标值在左半区 |
| 2 | 0 | 0 | 0 | 1 | 1<2 | left=1 | 目标值在右半区 |
| 循环结束 | 1 | 0 | - | - | - | - | left>right,返回1 |
关键细节说明
- mid计算优化:使用
left + (right-left)/2而非(left+right)/2,避免left+right超出int范围(如nums长度为1e4时无问题,但大数场景更安全); - 循环条件:必须是
left <= right,而非left < right,确保能检查到最后一个元素; - 结果正确性:
- 目标值存在:循环中会定位到该值,最终
left等于其索引; - 目标值不存在:
left指向第一个大于目标值的位置(示例3中target=7,最终left=4,即数组末尾的插入位置);
- 目标值存在:循环中会定位到该值,最终
- 无重复元素:题目明确nums无重复,无需处理重复值的边界情况。
性能说明
- 时间复杂度:O(logn)(每次循环将搜索范围缩小一半,最多循环log₂n次);
- 空间复杂度:O(1)(仅使用常数级别的额外变量);
- 优势:
- 严格满足题目O(logn)时间复杂度要求;
- 代码极简,仅需基础二分框架即可实现;
- 无需额外判断,循环结束直接返回left,逻辑闭环。
public int searchInsert(int[] nums, int target) {
int left=0;
int right=nums.length-1;
while(left<=right){
int mid=left+(right-left)/2;
int midVal=nums[mid];
if(midVal<target){
left=mid+1;
}else {
right=mid-1;
}
}
return left;
}
示例解答
解题思路
解法1:显式查找目标值的二分法(更易理解)
核心方法:在二分循环中显式判断是否找到目标值,找到则立即返回索引,未找到则最后返回left,逻辑与原解法一致,但更贴合“先查找、再插入”的直观思路。
代码实现
public int searchInsert(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
// 找到目标值,直接返回索引
return mid;
} else if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
// 未找到,返回插入位置
return left;
}
核心逻辑说明
- 显式匹配:增加
nums[mid] == target的判断,找到目标值时立即返回,减少不必要的循环; - 逻辑等价性:与原解法逻辑完全等价,仅拆分了判断条件,最终返回的left值一致;
- 可读性提升:新手更容易理解“先找目标值,找不到再返回插入位置”的流程。
性能说明
- 时间复杂度:O(logn)(最优情况找到目标值时提前返回,效率更高);
- 空间复杂度:O(1)(与原解法一致);
- 优势:
- 可读性更强,符合“查找→插入”的直观思维;
- 找到目标值时提前终止循环,减少迭代次数;
- 劣势:代码量略多一行判断,无本质性能差异。
解法2:左闭右开区间的二分法(进阶写法)
核心方法:将搜索区间定义为左闭右开[left, right),右边界初始为nums.length,通过调整指针移动规则实现二分查找,是二分查找的另一种经典范式。
代码实现
public int searchInsert(int[] nums, int target) {
int left = 0;
int right = nums.length; // 右边界为数组长度(左闭右开)
while (left < right) { // 循环条件为left < right
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1; // 左区间仍为闭区间
} else {
right = mid; // 右区间为开区间,不包含mid
}
}
// left == right,即为插入位置
return left;
}
核心逻辑说明
- 区间定义:
[left, right)表示当前搜索区间,初始为[0, nums.length),覆盖所有可能的插入位置(0到nums.length); - 循环条件:
left < right,因为右开区间无需等于; - 指针移动:
nums[mid] < target:目标值在右半区,left = mid + 1(左闭);nums[mid] ≥ target:目标值在左半区,right = mid(右开);
- 结果一致性:最终
left == right,值与原解法的left完全一致。
性能说明
- 时间复杂度:O(logn)(与原解法一致);
- 空间复杂度:O(1)(与原解法一致);
- 优势:
- 无需处理
right = mid - 1的减1操作,减少出错概率; - 区间定义更符合编程中“左闭右开”的常用习惯(如数组切片);
- 无需处理
- 劣势:需理解左闭右开的区间逻辑,新手入门门槛略高。
总结
- 二分查找边界定位法(第一次解答):O(logn)时间+O(1)空间,左闭右闭区间范式,代码极简,是本题的核心最优解;
- 显式查找目标值的二分法:O(logn)时间+O(1)空间,可读性更强,找到目标值时提前返回;
- 左闭右开区间的二分法:O(logn)时间+O(1)空间,进阶范式,减少指针减1操作;
- 关键技巧:
- 核心思想:插入位置本质是“第一个大于等于目标值的元素索引”,二分查找的核心是缩小该位置的范围;
- 区间选择:左闭右闭/左闭右开均可,关键是保持区间定义的一致性;
- 结果保证:无论哪种范式,最终返回的left都是正确的插入位置/目标值索引。