LeetCode 35. 搜索插入位置:二分查找的经典应用

0 阅读5分钟

刷LeetCode时,二分查找是高频考点,而35题「搜索插入位置」更是二分查找的入门级经典——它不仅考察对二分查找核心逻辑的理解,还需要灵活处理“目标值不存在”的边界场景。今天就来拆解这道题的解题思路,一步步看懂代码背后的逻辑,吃透O(log n)时间复杂度的实现要点。

题目回顾

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

核心约束:必须使用时间复杂度为 O(log n) 的算法

这里要注意,题目明确给出“排序数组”,这是二分查找能够适用的前提——二分查找的核心就是利用“有序”特性,每次将查找范围缩小一半,从而实现对数级的时间复杂度,比暴力遍历(O(n))高效得多。

解题思路:二分查找的边界处理

二分查找的关键的是「定义查找区间」和「处理边界条件」,这道题的难点在于:当目标值不在数组中时,如何精准找到它的插入位置。

我们先明确核心思路:

  1. 定义左右指针,划定查找区间(本题采用「左闭右闭」区间,即left ≤ right时,区间内仍有元素可查找);

  2. 计算中间位置mid,通过比较target和nums[mid]的大小,缩小查找范围;

  3. 用一个变量ans记录“可能的插入位置”,默认值设为数组长度n(对应目标值比所有元素都大的情况);

  4. 当target ≤ nums[mid]时,说明目标值可能在mid左侧(或就是mid),此时更新ans为mid,并将右指针左移(right = mid - 1);

  5. 当target > nums[mid]时,说明目标值在mid右侧,将左指针右移(left = mid + 1);

  6. 循环结束后,ans即为目标值的索引(存在时)或插入位置(不存在时)。

代码逐行解析

给出的代码简洁高效,我们逐行拆解,看懂每一步的作用:

function searchInsert(nums: number[], target: number): number {
  const n = nums.length;
  let left = 0, right = n - 1, ans = n;
  // 维护两个闭区间
  while (left <= right) {
    let mid = (left + right) >> 1;
    if (target <= nums[mid]) {
      ans = mid;
      right = mid - 1;
    } else {
      left = mid + 1;
    }
  }
  return ans;
};

1. 变量初始化

  • const n = nums.length;:记录数组长度,避免多次调用nums.length,提升效率;

  • let left = 0, right = n - 1;:定义左指针(指向数组起始位置)和右指针(指向数组末尾位置),确立「左闭右闭」的查找区间;

  • ans = n;:初始化插入位置为数组长度,这是一个“兜底”设置——当target比数组中所有元素都大时,插入位置就是数组末尾,即索引n。

2. 二分查找循环

while (left <= right):循环条件是左指针不大于右指针,这是「左闭右闭」区间的核心特征——当left == right时,区间内还有最后一个元素需要判断,避免遗漏。

3. 中间位置计算

let mid = (left + right) >> 1;:计算中间索引mid,这里用「右移1位」代替「除以2」,效率更高(二进制右移1位等价于整除2,且计算机处理位运算比除法更快)。

注意:也可以写成mid = Math.floor((left + right) / 2),两者效果一致,但右移写法更简洁高效。

4. 核心判断逻辑

这部分是整个算法的核心,负责缩小查找范围并更新插入位置:

  • if (target <= nums[mid])

如果目标值小于等于中间元素,说明目标值要么就是nums[mid],要么在mid的左侧区间。此时,我们将ans更新为mid(因为mid是当前可能的插入位置),然后将右指针right移到mid - 1,缩小查找范围到左半部分。

  • else

如果目标值大于中间元素,说明目标值在mid的右侧区间,此时不需要更新ans(因为当前mid不可能是插入位置),直接将左指针left移到mid + 1,缩小查找范围到右半部分。

5. 返回结果

return ans;:循环结束后,left会大于right,此时ans已经记录了目标值的索引(存在时)或正确的插入位置(不存在时)。

关键细节:为什么这样能找到插入位置?

很多人会疑惑,ans的更新逻辑为什么能精准定位插入位置?我们举两个例子理解:

  1. 示例1:nums = [1,3,5,6],target = 5(目标值存在)

循环过程中,当mid = 2(nums[mid] = 5),target <= nums[mid],ans更新为2,right = 1。之后循环结束,返回ans=2,正确。

  1. 示例2:nums = [1,3,5,6],target = 2(目标值不存在)
  • 初始left=0,right=3,ans=4;

  • mid=1(nums[mid]=3),target=2 <= 3,ans更新为1,right=0;

  • mid=0(nums[mid]=1),target=2 > 1,left=1;

  • 循环结束(left=1 > right=0),返回ans=1,即插入位置在1和3之间,正确。

本质上,ans始终记录着“当前最接近target且大于等于target的元素索引”,当循环结束时,这个索引就是插入位置(如果target不存在),或者就是target本身的索引(如果存在)。

总结

这道题的核心是「二分查找的边界处理」,重点掌握3点:

  • 「左闭右闭」区间的定义的循环条件(left <= right);

  • ans变量的作用——兜底插入位置,动态更新可能的插入点;

  • mid的计算方式(右移1位)和指针移动逻辑(缩小一半范围)。

二分查找的难点在于边界条件的处理,这道题作为入门题,能帮我们快速掌握二分查找的核心逻辑。记住:只要数组有序,就可以优先考虑二分查找,而「左闭右闭」区间是最容易理解和实现的方式之一。