全栈开发知识库

91.搜索插入位置

题目

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

请必须使用时间复杂度为 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] <= 104
  • nums 为 无重复元素 的 升序 排列数组
  • -104 <= target <= 104

思路

题目要求 O(log n),所以线性扫描直接出局,必须二分。

容易把这题写成"先二分找,找到返回下标;找不到再想办法算插入位置"——分两套逻辑,代码又长又容易错。

真相是:这两种情况的答案完全统一。

换个说法就明白了:

▎ 答案 = 数组中小于 target 的元素个数。

验证一下:

┌───────────┬────────┬────────────────────┬──────┬──────┐ │ nums │ target │ 小于 target 的元素 │ 个数 │ 答案 │ ├───────────┼────────┼────────────────────┼──────┼──────┤ │ [1,3,5,6] │ 5 │ 1, 3 │ 2 │ 2 ✅ │ ├───────────┼────────┼────────────────────┼──────┼──────┤ │ [1,3,5,6] │ 2 │ 1 │ 1 │ 1 ✅ │ ├───────────┼────────┼────────────────────┼──────┼──────┤ │ [1,3,5,6] │ 7 │ 1,3,5,6 │ 4 │ 4 ✅ │ ├───────────┼────────┼────────────────────┼──────┼──────┤ │ [1,3,5,6] │ 0 │ 无 │ 0 │ 0 ✅ │ └───────────┴────────┴────────────────────┴──────┴──────┘

四种情况(含两个边界)全部对上,找到和没找到根本不需要区分。

再换一个等价说法,这个说法直接对应代码:

▎ 答案 = 第一个「大于等于 target」的元素的下标。(如果都小于 target,就是 n)

这个东西有个标准名字叫 lower_bound(下界查找),是二分查找里最重要的一个变体。本题就是 lower_bound 的裸题。

为什么最后返回 left

这是全题最需要理解的地方。用循环不变量来看:

整个循环过程中,这两条始终成立:

  • left 左边的元素(下标 < left)全都 < target
  • right 右边的元素(下标 > right)全都 >= target

循环在 left > right 时结束,此时必然是 left == right + 1,两个区域正好拼接、没有缝隙:

下标: [ 0 ... left-1 ] [ left ... n-1 ] 状态: 全都 < target 全都 >= target ↑ 分界点就在这,left 就是答案

所以 left 恰好是"第一个 ≥ target 的位置",也恰好是"小于 target 的元素个数"。两种说法在这里合流了。

  class Solution {

      /**
       * 搜索插入位置 —— lower_bound 二分查找
       *
       * 核心思想:找到与找不到其实是同一个问题 ——
       *   答案 = 第一个 >= target 的元素下标 = 小于 target 的元素个数
       *
       * 循环不变量:
       *   left  左边(下标 < left)的元素全都 < target
       *   right 右边(下标 > right)的元素全都 >= target
       *   循环结束时 left == right + 1,两区正好拼接,left 即为分界点
       *
       * 时间复杂度 O(log n):区间每轮折半
       * 空间复杂度 O(1)
       */
      fun searchInsert(nums: IntArray, target: Int): Int {
          // 闭区间 [left, right],搜索范围包含两端
          var left = 0
          var right = nums.size - 1

          // 闭区间的循环条件必须是 <= :
          // left == right 时区间里还剩一个元素,不能漏掉对它的检查
          while (left <= right) {
              // 写成 left + (right - left) / 2 而不是 (left + right) / 2,
              // 后者在数组极长时 left + right 会整数溢出成负数 → 下标越界
              val mid = left + (right - left) / 2

              if (nums[mid] < target) {
                  // mid 太小,不可能是答案,答案在 mid 右边
                  left = mid + 1
              } else {
                  // nums[mid] >= target:mid 有可能是答案,
                  // 但左边也许还有更靠前的,继续往左收缩
                  right = mid - 1
              }
          }

          // 退出时 left 正好落在分界点上:
          // 它左边全 < target,它及右边全 >= target
          return left
      }
  }

复杂度

  • 时间复杂度:
  • 空间复杂度:


  • 邮箱 :charon.chui@gmail.com
  • Good Luck!
本页由 Markdown 自动渲染 · 共 628 篇笔记