刷二分查找的第一天,我信心满满。
不就是每次砍一半吗?有手就行。
结果这道题我提交了四次才过,全栽在边界条件上。
你是不是也这样,感觉自己懂二分,但一写就错,错了还不知道为啥?
今天就聊聊这道题,以及我踩过的那些坑。
题目说的啥
给定一个升序排列的数组和一个目标值。
找到了就返回索引,没找到就返回它应该插入的位置。
要求时间复杂度 O(log n) ------ 说白了就是不让你从头到尾遍历,逼你用二分。
举个例子:nums = [1,3,5,6], target = 2,输出是 1,因为 2 应该插在 3 前面。
我一开始的想法
说实话,刚看到这题我第一反应是:
遍历一遍不就完了?找到第一个大于等于 target 的位置,返回它的索引。
javascript
var searchInsert = function(nums, target) {
for (let i = 0; i < nums.length; i++) {
if (nums[i] >= target) return i
}
return nums.length
};
写完我还挺得意,多简洁啊。
然后一看题目要求:O(log n) 。
行吧,暴力解法虽然能过,但不符合题意。而且面试的时候你这么写,面试官大概率会追问:"能不能优化一下?"
我是怎么想到二分的
你想啊,数组是有序 的,还要求O(log n) ------ 这两个条件放在一起,答案基本就写在脸上了:二分查找。
但问题来了,普通的二分查找是 "找到了返回索引,找不到返回 -1"。
这道题不一样,找不到的时候要返回插入位置。
这个插入位置到底是啥?说白了就是:第一个大于等于 target 的元素的索引。
想通了这一点,二分的思路就清晰了。
手算一遍找找感觉
拿 nums = [1,3,5,6], target = 2 来走一遍。
一开始 l = 0, r = 3,中间位置 mid = 1,对应的值是 3。
3 比 2 大,说明 target 在左边,r 往左移:r = 0。
现在 l = 0, r = 0,mid = 0,对应的值是 1。
1 比 2 小,说明 target 在右边,l 往右移:l = 1。
这时候 l > r 了,循环结束。
返回谁?返回 l,也就是 1------ 正好是正确答案。
哎?为什么返回 l 而不是 r?
这个问题我当时想了好久。
关键:为什么返回 l
我教你一个记住的办法:
循环结束的时候,l 和 r 的关系是 l = r + 1。
你想啊,每次循环我们都在缩小范围:
- 中间值比 target 小 → target 在右边 → l = mid + 1
- 中间值大于等于 target → target 在左边 → r = mid - 1
当循环结束时,l 指向的就是第一个大于等于 target 的位置。
而 r 指向的是最后一个小于 target 的位置。
所以插入位置当然是 l 呀。
不信你可以再试几个例子,保证都对。
代码实现
javascript
var searchInsert = function(nums, target) {
// 左右指针,分别指向数组首尾
let l = 0, r = nums.length - 1
// 注意这里是 <=,不是 <
// 写成 < 的话,l 和 r 重合的那个元素就没被检查到
while (l <= r) {
// 用位运算右移一位代替除以2再取整,效果一样但更装逼
// 等价于 Math.floor((l + r) / 2)
const mid = (l + r) >> 1
if (nums[mid] < target) {
// 中间值比目标小,目标肯定在右边
// l 直接跳到 mid + 1,mid 本身已经排除了
l = mid + 1
} else {
// 中间值 >= 目标,目标在左边(或者就是 mid 本身)
// 为什么不用单独处理等于的情况?
// 因为就算找到了,继续往左缩也不影响最终结果
// 最后 l 还是会停在正确位置上
r = mid - 1
}
}
// 循环结束时 l = r + 1
// l 指向第一个 >= target 的位置,正好就是插入位置
// 别问我为什么知道,问就是在这里卡了半小时
return l
};
我踩过的坑
说几个我自己犯过的错,看看你有没有中招。
第一个坑:循环条件写成 l < r。
这样写的话,当 l 和 r 指向同一个元素时,循环就结束了,那个元素根本没被检查过。结果就是有时候会漏掉正确答案。
第二个坑:r 初始值写成 nums.length。
也不是不行,但对应的边界条件全要改,很容易搞混。我推荐统一用 nums.length - 1,配合 l <= r,最不容易错。
第三个坑:返回 r。
我第一次写的时候想当然返回了 r,结果一跑全错。后来才想明白,插入位置是 l 指向的地方。
复杂度分析
时间复杂度 O (log n),每次砍掉一半,经典二分。
空间复杂度 O (1),就几个变量。
最后说两句
这道题看起来简单,但真的很考验你对二分查找边界的理解。
我当时刷完的感受是:二分查找不在于 "会不会",而在于 "能不能一次写对"。
建议你把这道题的模板背下来,后面刷二分的题目基本都能套用。
你有没有在二分查找的边界条件上翻过车?或者你有更巧妙的写法?评论区聊聊,我会一条一条看的。
如果觉得有帮助,点个赞让更多人看到,下次刷二分就不会懵了~