📖 第55课:搜索插入位置

3 阅读13分钟

想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。

📖 第55课:搜索插入位置

模块:二分查找 | 难度:Easy ⭐⭐ LeetCode 链接:leetcode.cn/problems/se… 前置知识:第54课 二分查找 预计学习时间:15分钟


🎯 题目描述

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

示例:

输入:nums = [1,3,5,6], target = 5
输出:2
解释:5 在数组中,索引为2

输入:nums = [1,3,5,6], target = 2
输出:1
解释:2 应该插入在索引1的位置

约束条件:

  • 1 ≤ nums.length ≤ 10^4
  • -10^4 ≤ nums[i] ≤ 10^4
  • nums 为无重复元素的升序排列数组
  • -10^4 ≤ target ≤ 10^4

🧪 边界用例(面试必考)

用例类型输入期望输出考察点
最小输入nums=[1], target=10单元素精确匹配
插入头部nums=[1,3,5], target=00插入位置在开头
插入尾部nums=[1,3,5], target=73插入位置在末尾
插入中间nums=[1,3,5,6], target=42插入位置在中间
精确匹配nums=[1,3,5,6], target=31目标值存在

💡 思路引导

生活化比喻

想象你要把一本新书插入到书架上,书架上的书已经按照书名字母排序好了。

🐌 笨办法:从头开始逐本检查,直到找到第一本字母比新书大的书,把新书插在它前面。如果书架有1000本书,最坏情况要检查1000次。

🚀 聪明办法:用"二分查找"思想 — 先看中间的书,如果新书比它小,就只看左半边,否则看右半边。每次都能排除一半的书,1000本书最多只需要检查10次(log₂1000 ≈ 10)。

关键洞察

本题的核心是"左边界二分查找" — 找到第一个大于等于target的位置,这个位置就是插入位置!


🧠 解题思维链

这一节模拟你在面试中"从零开始思考"的过程。

Step 1:理解题目 → 锁定输入输出

  • 输入:有序数组nums + 目标值target
  • 输出:返回索引(如果找到) 或 插入位置(如果不存在)
  • 限制:必须用O(log n)算法 → 提示用二分查找

Step 2:先想笨办法(暴力法)

从头到尾遍历数组,找到第一个大于等于target的位置:

for i in range(len(nums)):
    if nums[i] >= target:
        return i
return len(nums)  # 如果都小于target,插入末尾
  • 时间复杂度:O(n)
  • 瓶颈在哪:没有利用"有序"这个特性,逐个检查太慢

Step 3:瓶颈分析 → 优化方向

暴力法中每次只排除1个元素,效率低下。

  • 核心问题:"有序数组"的信息被浪费了
  • 优化思路:能不能每次排除一半的元素? → 用二分查找

Step 4:选择武器

  • 选用:左边界二分查找
  • 理由:我们要找的是"第一个大于等于target的位置",这正是左边界二分的定义

🔑 模式识别提示:当题目出现"有序数组 + O(log n)要求",优先考虑"二分查找"


🔑 解法一:标准二分查找(直觉法)

思路

使用标准二分查找尝试找到target,如果没找到,根据最后的left指针位置返回插入点。

图解过程

示例: nums = [1,3,5,6], target = 5

初始状态:
  L           M           R
  ↓           ↓           ↓
[ 1,    3,    5,    6 ]
mid=1, nums[1]=3 < 5, 往右找

第2轮:
              L     M     R
              ↓     ↓     ↓
[ 1,    3,    5,    6 ]
mid=2, nums[2]=5 == 5, 找到了! 返回2


示例2: nums = [1,3,5,6], target = 2

初始状态:
  L           M           R
  ↓           ↓           ↓
[ 1,    3,    5,    6 ]
mid=1, nums[1]=3 > 2, 往左找

第2轮:
  L     R
  ↓     ↓
[ 1,    3,    5,    6 ]
  M=0
mid=0, nums[0]=1 < 2, 往右找

第3轮:
        L
        R
        ↓
[ 1,    3,    5,    6 ]
left > right, 循环结束, 返回left=1 (插入位置)

Python代码

from typing import List


def searchInsert(nums: List[int], target: int) -> int:
    """
    解法一:标准二分查找
    思路:用二分查找,如果找到返回索引,没找到返回left指针位置
    """
    left, right = 0, len(nums) - 1

    while left <= right:
        mid = left + (right - left) // 2  # 防止溢出

        if nums[mid] == target:
            return mid  # 找到目标值
        elif nums[mid] < target:
            left = mid + 1  # 目标在右半边
        else:
            right = mid - 1  # 目标在左半边

    # 循环结束时, left就是插入位置
    return left


# ✅ 测试
print(searchInsert([1, 3, 5, 6], 5))  # 期望输出:2
print(searchInsert([1, 3, 5, 6], 2))  # 期望输出:1
print(searchInsert([1, 3, 5, 6], 7))  # 期望输出:4
print(searchInsert([1, 3, 5, 6], 0))  # 期望输出:0

复杂度分析

  • 时间复杂度:O(log n) — 每次循环排除一半元素
    • 具体地说:如果输入规模 n=10000,大约需要 log₂10000 ≈ 14 次比较
  • 空间复杂度:O(1) — 只用了几个指针变量

优缺点

  • ✅ 思路直观,容易理解
  • ✅ 时间复杂度已达最优O(log n)
  • ⚠️ 需要理解为什么循环结束时left就是插入位置

🏆 解法二:左边界二分查找(最优解)

优化思路

用"左边界二分"的标准模板,直接找到"第一个大于等于target的位置",代码更简洁统一。

💡 关键想法:插入位置 = 第一个大于等于target的索引,这正是左边界二分的定义!

图解过程

示例: nums = [1,3,5,6], target = 2

左边界二分的核心思想:
"找到第一个 >= target 的位置"

  L                       R
  ↓                       ↓
[ 1,    3,    5,    6 ] (范围: 左闭右开 [0, 4))
        M=2
nums[2]=5 >= 2, 可能是答案, 继续往左找

  L           R
  ↓           ↓
[ 1,    3,    5,    6 ] (范围: [0, 2))
  M=1
nums[1]=3 >= 2, 可能是答案, 继续往左找

  L     R
  ↓     ↓
[ 1,    3,    5,    6 ] (范围: [0, 1))
  M=0
nums[0]=1 < 2, 不是答案, 往右找

        L
        R
        ↓
[ 1,    3,    5,    6 ] (范围: [1, 1))
left == right, 结束, 返回left=1

结论: 第一个 >= 2 的位置是索引1 (元素3)

Python代码

def searchInsert_v2(nums: List[int], target: int) -> int:
    """
    解法二:左边界二分查找 (最优解)
    思路:找到第一个大于等于target的位置
    """
    left, right = 0, len(nums)  # 注意: right = len(nums), 左闭右开区间

    while left < right:  # 注意: 不带等号
        mid = left + (right - left) // 2

        if nums[mid] < target:
            left = mid + 1  # [mid+1, right) 区间继续找
        else:
            # nums[mid] >= target, mid可能是答案, 保留它
            right = mid  # [left, mid) 区间继续找

    # 循环结束时 left == right, 就是插入位置
    return left


# ✅ 测试
print(searchInsert_v2([1, 3, 5, 6], 5))  # 期望输出:2
print(searchInsert_v2([1, 3, 5, 6], 2))  # 期望输出:1
print(searchInsert_v2([1, 3, 5, 6], 7))  # 期望输出:4
print(searchInsert_v2([1, 3, 5, 6], 0))  # 期望输出:0

复杂度分析

  • 时间复杂度:O(log n) — 与解法一相同
  • 空间复杂度:O(1) — 与解法一相同

为什么是最优解:

  • 时间O(log n)已经是理论最优(必须至少检查log n个元素来定位)
  • 空间O(1)也是最优(原地操作)
  • 代码模板统一,适用于所有边界查找问题(左边界、右边界)

🐍 Pythonic 写法

利用 Python 的 bisect 模块(标准库内置的二分查找):

import bisect

def searchInsert_pythonic(nums: List[int], target: int) -> int:
    """
    Pythonic写法:使用bisect.bisect_left
    bisect_left(nums, target) 返回第一个 >= target 的位置
    """
    return bisect.bisect_left(nums, target)


# ✅ 测试
print(searchInsert_pythonic([1, 3, 5, 6], 5))  # 期望输出:2
print(searchInsert_pythonic([1, 3, 5, 6], 2))  # 期望输出:1

说明:

  • bisect.bisect_left(nums, target) 本质就是左边界二分查找
  • 一行代码解决问题,但面试时仍需手写以展示算法理解

⚠️ 面试建议:先手写解法二展示二分查找功底,通过后再提bisect展示对Python标准库的了解。 面试官更看重你的思考过程,而非代码行数。


📊 解法对比

维度解法一:标准二分🏆 解法二:左边界二分(最优)Pythonic:bisect
时间复杂度O(log n)O(log n)O(log n)
空间复杂度O(1)O(1)O(1)
代码难度中等(需理解返回值)简单(模板统一)极简(一行)
面试推荐⭐⭐⭐⭐⭐ ← 首选⭐(辅助)
适用场景精确查找为主所有边界查找问题Python快速实现

为什么解法二是最优:

  • 时间空间都已达理论最优O(log n) / O(1)
  • 代码模板统一,可以直接套用到"查找左边界/右边界"等变体题
  • 逻辑清晰:"找第一个>=target" 直接对应题意

面试建议:

  1. 先用30秒口述暴力法思路(O(n)遍历),表明你理解题意
  2. 立即优化到🏆解法二(左边界二分),重点讲解"为什么left就是插入位置"
  3. 手动测试边界用例:target比所有元素小/大、target在中间不存在、target存在
  4. 如果时间充裕,提一下bisect模块展示Python功底

🎤 面试现场

模拟面试中的完整对话流程,帮你练习"边想边说"。

面试官:请你解决一下这道题。

:(审题30秒)好的,这道题要求在有序数组中找到target或返回插入位置,并且要求O(log n)时间复杂度。让我先想一下...

我的第一个想法是暴力遍历,从头到尾找第一个大于等于target的位置,时间复杂度是O(n)。

不过题目要求O(log n),这提示我们要用二分查找。核心洞察是:插入位置就是"第一个大于等于target的索引",这正好是左边界二分查找的定义。

面试官:很好,请写一下代码。

:(边写边说)我用左闭右开区间[left, right)来写,这样边界处理更统一:

  • 初始化left=0, right=len(nums)
  • 每次取中点mid,如果nums[mid] < target,说明答案在右边,更新left=mid+1
  • 如果nums[mid] >= target,说明mid可能是答案,保留它,更新right=mid
  • 循环结束时left就是答案

面试官:测试一下?

:用示例[1,3,5,6], target=2走一遍:

  • 初始left=0, right=4, mid=2, nums[2]=5 >= 2, 更新right=2
  • left=0, right=2, mid=1, nums[1]=3 >= 2, 更新right=1
  • left=0, right=1, mid=0, nums[0]=1 < 2, 更新left=1
  • left=1, right=1, 循环结束, 返回1 ✅

再测边界情况target=7:

  • 会一直往右找,最终left=4,正好是数组长度,表示插入末尾 ✅

高频追问

追问应答策略
"还有更优解吗?"时间O(log n)已经是理论最优,因为二分每次排除一半,不可能更快。空间O(1)也是最优。
"为什么循环结束时left就是插入位置?"因为我们维护的不变量是:[left, right)区间内所有元素都 < target, right及右边的元素都 >= target。循环结束时left==right,就是第一个 >= target 的位置。
"如果有重复元素怎么办?"当前解法会返回第一个target的位置,如果要找最后一个,用右边界二分。
"能用Python标准库吗?"可以用bisect.bisect_left(nums, target),一行解决,但手写更能展示算法理解。

🎓 知识点总结

Python技巧卡片 🐍

# 技巧1:防止整数溢出的中点计算
mid = left + (right - left) // 2  # 推荐,防止 left+right 溢出
# 而不是: mid = (left + right) // 2

# 技巧2:bisect模块 — Python内置二分查找
import bisect
pos = bisect.bisect_left(nums, target)   # 左边界:第一个 >= target
pos = bisect.bisect_right(nums, target)  # 右边界:第一个 > target
bisect.insort_left(nums, target)         # 插入并保持有序

💡 底层原理(选读)

为什么二分查找这么快?

二分查找的时间复杂度O(log n)来自于"每次排除一半"的策略:

  • 1000个元素 → 500 → 250 → 125 → 63 → 32 → 16 → 8 → 4 → 2 → 1
  • 只需要10次就能定位,而遍历需要1000次
  • 对于10^4个元素,二分只需14次,对于10^9个元素,只需30次!

左闭右开 vs 左闭右闭?

  • 左闭右开[left, right):循环条件left < right,更新right = mid
  • 左闭右闭[left, right]:循环条件left <= right,更新right = mid - 1
  • 两种都正确,推荐左闭右开,因为边界处理更统一(right直接等于长度)

算法模式卡片 📐

  • 模式名称:左边界二分查找
  • 适用条件:有序数组 + 查找"第一个满足条件"的元素
  • 识别关键词:"有序"、"第一个大于等于"、"插入位置"、"O(log n)"
  • 模板代码:
def left_bound(nums: List[int], target: int) -> int:
    """左边界二分:找第一个 >= target 的位置"""
    left, right = 0, len(nums)  # 左闭右开
    while left < right:
        mid = left + (right - left) // 2
        if nums[mid] < target:
            left = mid + 1
        else:
            right = mid  # 保留mid,继续往左找
    return left

易错点 ⚠️

  1. 边界条件混淆

    • 错误:while left <= right 配合 right = len(nums) → 会越界
    • 正确:左闭右开用<,左闭右闭用<=
  2. 返回值理解错误

    • 错误:以为left只在找到target时才是正确答案
    • 正确:循环结束时,left永远是"第一个 >= target 的位置",找不到时就是插入点
  3. mid计算溢出(Python不会溢出,但C++/Java会)

    • 错误:mid = (left + right) // 2 → left+right可能溢出
    • 正确:mid = left + (right - left) // 2

🏗️ 工程实战(选读)

这个算法思想在真实项目中的应用,让你知道"学了有什么用"。

  • 场景1:数据库索引 — MySQL的B+树索引本质就是多路平衡二分查找,将O(n)的顺序扫描优化为O(log n)
  • 场景2:Git bisect命令 — 用二分法快速定位引入bug的提交,从几千个commit中只需十几次就能找到
  • 场景3:版本控制 — 在有序的版本号列表中快速定位某个功能首次出现的版本
  • 场景4:推荐系统 — 在按评分排序的商品列表中,快速找到第一个评分>=4星的商品

🏋️ 举一反三

完成本课后,试试这些同类题目来巩固知识:

题目难度相关知识点提示
LeetCode 34. 在排序数组中查找元素的首末位置Medium左右边界二分分别用左边界和右边界二分查找
LeetCode 69. x的平方根Easy二分查找答案在[0, x]区间二分查找答案
LeetCode 278. 第一个错误版本Easy左边界二分找第一个返回true的版本
LeetCode 153. 寻找旋转排序数组中的最小值Medium二分变体判断哪半边有序来调整指针

📝 课后小测

试试这道变体题,不要看答案,自己先想5分钟!

题目:给定有序数组nums和目标值target,找到target在数组中最后一次出现的位置,如果不存在返回-1。例如nums=[5,7,7,8,8,10], target=8,返回4。

💡 提示(实在想不出来再点开)

这是"右边界二分查找" — 找到最后一个等于target的位置,即"最后一个 <= target 的位置"。

✅ 参考答案
def searchLast(nums: List[int], target: int) -> int:
    """
    右边界二分:找最后一个 <= target 的位置
    """
    left, right = 0, len(nums)

    while left < right:
        mid = left + (right - left) // 2
        if nums[mid] <= target:
            left = mid + 1  # 继续往右找
        else:
            right = mid

    # left-1 是最后一个 <= target 的位置
    if left > 0 and nums[left - 1] == target:
        return left - 1
    return -1


# 测试
print(searchLast([5, 7, 7, 8, 8, 10], 8))  # 输出:4

核心思路:右边界二分找"最后一个 <= target",然后检查是否等于target。注意返回的是left-1而不是left


如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。