二分查找专题(二):lower_bound 的首秀——精解「搜索插入位置」

哈喽各位,我是前端小L。

欢迎来到我们的二分查找专题第二篇!上一节(LC 704),我们的任务很简单:找到,或者放弃。if (nums[mid] == target) 是我们最关心的代码。

但今天,target 即使不存在,我们也必须给它"在有序的江湖里找一个位置 "。这个问题,不再是简单的 == 判断,而是要我们找到一个"分界点":

  • 这个点左边 的所有元素,都小于 target

  • 这个点及它右边 的所有元素,都大于等于 target

这个"分界点",就是大名鼎鼎的 lower_bound(C++ STL中的函数),它也是我们"万能模板"的天然归宿。

力扣 35. 搜索插入位置

https://leetcode.cn/problems/search-insert-position/

题目分析: 给定一个排序数组 nums 和一个目标值 target

  1. 如果 target 存在,返回其索引

  2. 如果 target 不存在,返回它将会被按顺序插入的索引。 (题目保证数组中无重复元素)

例子:

  • nums = [1, 3, 5, 6], target = 5 -> target 存在,返回 2

  • nums = [1, 3, 5, 6], target = 2 -> target 不存在,应插入到 3 之前,返回 1

  • nums = [1, 3, 5, 6], target = 7 -> target 不存在,应插入到末尾,返回 4

  • nums = [1, 3, 5, 6], target = 0 -> target 不存在,应插入到开头,返回 0

核心洞察: 仔细观察所有例子,我们发现,我们要找的,其实就是数组中第一个大于或等于 target 的元素的索引

  • target = 5:第一个 >= 5 的是 5,索引 2

  • target = 2:第一个 >= 2 的是 3,索引 1

  • target = 7:第一个 >= 7 的元素不存在,此时"插入点"是 n(即 4)。

  • target = 0:第一个 >= 0 的是 1,索引 0

"万能模板"的真正威力

现在,让我们请出上一节的"万能模板": left = 0, right = n while (left < right) mid = ... if (nums[mid] < target) -> left = mid + 1 else (nums[mid] >= target) -> right = mid

让我们思考一下,当这个循环结束 时(即 left == right 时),left 指向的是什么?

  • left 指针(left = mid + 1)会跳过 所有严格小于 target 的元素。

  • right 指针(right = mid)会保留 所有大于或等于 target 的元素作为"候选答案"。

  • 当它们相遇时,left(或 right)所指向的位置,恰好就是第一个 nums[i] >= target 的位置

"Aha!"时刻: 我们上一节的"万能模板",根本不需要任何修改 !它本身就是一个完美的 lower_bound(寻找第一个 >= target)的实现!LC 704 中的 if (nums[mid] == target) 只不过是一个"提前退出"的优化。

套用"万能模板"

我们把 LC 704 的代码拿过来,只改动最后返回的部分。

复制代码
#include <vector>

using namespace std;

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int n = nums.size();
        
        // 1. 区间定义:[left, right) -> [0, n)
        int left = 0;
        int right = n; 

        // 2. 循环条件:left < right
        while (left < right) {
            // 3. mid 计算
            int mid = left + (right - left) / 2;

            if (nums[mid] < target) {
                // 目标在右侧 [mid + 1, right)
                left = mid + 1;
            } else { // nums[mid] >= target
                // 目标在左侧 [left, mid),或者 mid 就是答案
                right = mid; 
            }
        }
        
        // 循环结束时,left == right
        // left 指向的就是第一个 >= target 的位置
        return left;
    }
};

(注:我们甚至可以把 LC 704 的 if (nums[mid] == target) 判断去掉,逻辑依然完美。)

深度复杂度分析

  • 时间复杂度 O(log n)

    • 每次循环都将搜索空间 [left, right) 缩小一半。

    • 寻找一个 n 个元素的数组的边界,需要 log₂n 次。

  • 空间复杂度 O(1)

    • 只使用了 left, right, mid, n 等常数个额外变量。

模板演练(手把手走一遍)

nums = [1, 3, 5, 6], target = 2

  1. 初始化n = 4, left = 0, right = 4。搜索区间 [0, 4)

  2. 第1轮:

    • while (0 < 4): True

    • mid = 0 + (4 - 0) / 2 = 2

    • nums[2]5

    • 5 >= 2 (nums[mid] >= target)。

    • 答案在左侧 [left, mid)right = mid = 2

    • 新区间 [0, 2)

  3. 第2轮:

    • while (0 < 2): True

    • mid = 0 + (2 - 0) / 2 = 1

    • nums[1]3

    • 3 >= 2 (nums[mid] >= target)。

    • 答案在左侧 [left, mid)right = mid = 1

    • 新区间 [0, 1)

  4. 第3轮:

    • while (0 < 1): True

    • mid = 0 + (1 - 0) / 2 = 0

    • nums[0]1

    • 1 < 2 (nums[mid] < target)。

    • 答案在右侧 [mid + 1, right)left = mid + 1 = 1

    • 新区间 [1, 1)

  5. 第4轮:

    • while (1 < 1): False。循环终止。
  6. 返回 left,即 1。完全正确!

总结

今天,我们领略了"万能模板"在处理边界查找 时的惊人威力。 我们甚至不需要修改它,它天生就是为 lower_bound(寻找第一个 >= target)而设计的。

  • left = mid + 1 :是因为我们确定 mid 以及 mid 左侧的所有元素都不是答案。

  • right = mid :是因为 mid 可能 是答案(第一个 >= target 的),我们不能 把它排除掉(mid-1),只能将搜索上界缩到 mid

掌握了这个思想,下一题,我们将用它来解决一个更棘手的问题:在一个有重复元素的数组中,寻找目标的"第一个"和"最后一个"位置。

下期见!

相关推荐
老黄编程3 小时前
三维空间圆柱方程
算法·几何
xier_ran4 小时前
关键词解释:DAG 系统(Directed Acyclic Graph,有向无环图)
python·算法
CAU界编程小白4 小时前
数据结构系列之十大排序算法
数据结构·c++·算法·排序算法
执携4 小时前
数据结构 -- 树(遍历)
数据结构
好学且牛逼的马4 小时前
【Hot100 | 6 LeetCode 15. 三数之和】
算法
橘颂TA4 小时前
【剑斩OFFER】算法的暴力美学——二分查找
算法·leetcode·面试·职场和发展·c/c++
lkbhua莱克瓦244 小时前
Java基础——常用算法4
java·数据结构·笔记·算法·github·排序算法·快速排序
m0_748248025 小时前
揭开 C++ vector 底层面纱:从三指针模型到手写完整实现
开发语言·c++·算法
七夜zippoe5 小时前
Ascend C流与任务管理实战:构建高效的异步计算管道
服务器·网络·算法