没想到一个小小的二分法,让我犯这么多错

66 阅读2分钟

搜索插入位置

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

典型二分法。二分查找思路很简单,排序后的数据,假设数组中间数据比目标大,那么结果一定在数组前半段。反之,如果中间数据比目标小,那么结果一定在后半段。同时还要兼顾,数组中全部数据都比目标大或小的情况。

典型且简单,很适合用来归纳思考方式。

function main(nums, target) {
  // 左右指针
  let left = 0,
    right = nums.length - 1;
  // 结果,假设全部数据都小于,需要插入尾部
  let result = nums.length;
  while (left <= right) {
    // 二分,防止溢出
    const half = left + Math.floor((right - left) / 2);
    const halfVal = nums[half];
    // 相等获取 index
    if (halfVal === target) return half;
    // 大于证明,需要向左找
    // 假设全部元素都大于,最终遍历结束, left = right = 0
    if (halfVal > target) {
      right = half - 1;
      // 假设找不到,需要保留最后一个大于 target 的位置
      result = half;
    } else {
      // 小于证明,需要向右找
      // 假设全部都小于, left = right = nums.length
      // 一直小于没有关系,默认情况数组所有值都小于目标
      left = half + 1;
    }
  }
  return result;
}

查找中间点操作,也有快慢指针的方式查找。O(n) 操作,放在这里得不偿失。一开始想到的就是 (left + right) / 2 取整,这样计算有溢出风险。假设 right 值超出数字上限一半以上,二分收束到最右边会出现left + right > Number.MIN_VALUE。所以转换成,left + Math.floor((right - left) / 2),这样即使 right 已经到达 Number.MIN_VALUE,这里所有的操作也都低于最大值。

比较后,中间位置和目标值大小关系确定。下次比较的区间,无需包含中间位置,如 right = half - 1;。包含会有问题,假设 nums = [1, 2], target = 2,初始位置 left = 0, right = 1, half = 0,假设 left = half; 就会变成一个死循环。

另外一定记得考虑,全部大于目标或者小于的情况,代码中有注释。

总结,考虑二分,先考虑全部找不到的情况,包括查找到最右侧,和最左侧。然后开始思考,一两个元素的情况,看指针式怎么移动的。