给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 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 <= 104-104 <= nums[i] <= 104nums为 无重复元素 的 升序 排列数组-104 <= target <= 104
1. 生活案例:在图书馆书架插书
想象你在图书馆管理一个已经按编号排好序的书架:
-
规则:书架上的书从左到右编号越来越大。你手里有一本新书,编号是
target。 -
任务:
- 如果书架上已经有这本书了,直接找到它的位置。
- 如果没有这本书,你要找到它应该插入的位置,保证插入后书架依然是有序的。
-
过程:
- 你来到书架中间(mid),发现中间的书编号比你的大,于是你去左半边找;如果比你的小,就去右半边找。
- 重点来了:当你左右找遍了也没发现这编号,你最后停下来的那个地方,就是你把新书挤进去的位置。
2. 代码实现与详细注释
这是你图片中的代码,我为你添加了详细的中文注释。注意看最后 return left 的奥秘:
JavaScript
/**
* @param {number[]} nums - 已经排好序的书架
* @param {number} target - 你手里书的编号
* @return {number} - 找到的位置或应该插入的位置
*/
var searchInsert = function(nums, target) {
let left = 0;
let right = nums.length - 1;
// 标准二分查找模板
while (left <= right) {
// 取中间点,防止溢出的写法
let mid = Math.floor(left + (right - left) / 2);
if (nums[mid] === target) {
// 1. 运气好,书架上刚好有这编号,直接返回位置
return mid;
} else if (nums[mid] < target) {
// 2. 中间的书编号小了,说明你要找的位置在右边
left = mid + 1;
} else {
// 3. 中间的书编号大了,说明你要找的位置在左边
right = mid - 1;
}
}
// 4. 【核心点】:循环结束还没找到。
// 此时 left 指向的位置,正好就是这个数字“应该在”的位置。
// 比如在 [1, 3, 5, 6] 中找 2,循环结束时 left 会停在索引 1 的位置。
return left;
};
3. 核心原理解析
为什么找不到时返回 left?
这是二分查找的一个神奇性质。当 while (left <= right) 结束时:
right会停在比target小的最后一个元素上。left会停在比target大的第一个元素上。- 既然我们要插入
target且保持有序,它理所当然应该排在“比它大的第一个元素”的位置上,把原来的元素往后挤。所以return left就是我们要的答案。
各种情况演示:
以 nums = [1, 3, 5, 6] 为例:
-
target = 5:直接在
mid匹配到,返回 2。 -
target = 2:
- 第一次:mid=3,3 > 2,往左找,right 变成索引 0。
- 第二次:mid=1,1 < 2,往右找,left 变成索引 1。
- 此时 left > right,退出,返回 left (1) 。
-
target = 7(插在最后):
left会一路增加到索引 4,返回 4。 -
target = 0(插在最前):
right会一路减小到 -1,返回 left (0) 。
复杂度分析
- 时间复杂度:。每次比较都排除掉一半的范围。
- 空间复杂度:。只用了两个指针。