LC 35 搜索插入位置:二分查找谁都会,边界条件谁写谁懵

刷二分查找的第一天,我信心满满。

不就是每次砍一半吗?有手就行。

结果这道题我提交了四次才过,全栽在边界条件上。

你是不是也这样,感觉自己懂二分,但一写就错,错了还不知道为啥?

今天就聊聊这道题,以及我踩过的那些坑。

题目说的啥

给定一个升序排列的数组和一个目标值。

找到了就返回索引,没找到就返回它应该插入的位置。

要求时间复杂度 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 = 0mid = 0,对应的值是 1。

1 比 2 小,说明 target 在右边,l 往右移:l = 1

这时候 l > r 了,循环结束。

返回谁?返回 l,也就是 1------ 正好是正确答案。

哎?为什么返回 l 而不是 r?

这个问题我当时想了好久。

关键:为什么返回 l

我教你一个记住的办法:

循环结束的时候,lr 的关系是 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),就几个变量。

最后说两句

这道题看起来简单,但真的很考验你对二分查找边界的理解。

我当时刷完的感受是:二分查找不在于 "会不会",而在于 "能不能一次写对"。

建议你把这道题的模板背下来,后面刷二分的题目基本都能套用。

你有没有在二分查找的边界条件上翻过车?或者你有更巧妙的写法?评论区聊聊,我会一条一条看的。

如果觉得有帮助,点个赞让更多人看到,下次刷二分就不会懵了~

相关推荐
码哥DFS1 小时前
二叉树的直径
开发语言·javascript·算法
光影少年3 小时前
RN的Fabric 渲染流程
运维·前端·javascript·react native·react.js·fabric
营养充电站4 小时前
KMP全栈开发:从Android到AI Agent的技术演进与实践
人工智能·算法·docker·jupyter
技术小黑5 小时前
RNN算法实战系列05 | 天气预测
人工智能·rnn·算法
用户938515635075 小时前
React Router 进阶:路由守卫、登录鉴权与状态传递
前端·javascript·全栈
kyriewen5 小时前
我重写了自己用了两年的防抖节流Hook——发现里面藏着3个隐藏bug
前端·javascript·面试
漂流瓶jz6 小时前
Webpack开发环境:观察模式/webpack-dev-server/HMR热更新
前端·javascript·webpack
小爬的老粉丝6 小时前
Vue 3 文件预览生产排障:Worker/WASM 404、鉴权 Blob 与子路径
javascript·vue.js·wasm