编码母题 / P0 / 经典算法 / 03

二分查找左右边界

重要程度必练难度中等预计完成25–35 分钟

题目是什么

输入非降序数字数组 nums 和目标值 target,返回 target 首次与最后一次出现的索引 [first,last];不存在时返回 [-1,-1]。要求 O(log n) 时间;空数组也应返回 [-1,-1]。

输入输出示例

输入 [5,7,7,8,8,10] 和 8,输出 [3,4];输入 [] 和 1,输出 [-1,-1]。

练习起手式与验证

/*
 * 题目:二分查找左右边界
 * 要求:输入非降序数字数组 nums 和目标值 target,返回 target 首次与最后一次出现的索引 [first,last];不存在时返回 [-1,-1]。要求 O(log n) 时间;空数组也应返回 [-1,-1]。
 * 示例:输入 [5,7,7,8,8,10] 和 8,输出 [3,4];输入 [] 和 1,输出 [-1,-1]。
 */
function searchRange(nums, target) {}
console.log(searchRange([5, 7, 7, 8, 8, 10], 8)); // 预期:[3, 4]
console.log(searchRange([5, 7, 7, 8, 8, 10], 6)); // 预期:[-1, -1]
console.log(searchRange([], 1)); // 预期:[-1, -1]
console.log(searchRange([2, 2, 2], 2)); // 预期:[0, 2]

解题思路与实现

展开解题思路

对同一个数组做两次二分。找到 target 时先记下索引;找首次位置就继续查左半边,找最后位置就继续查右半边。循环结束后返回两次记录的结果。

function searchRange(nums, target) {
  const main = (nums, target, type) => {
    let left = 0;
    let right = nums.length - 1;
    let result = -1;

    while (left <= right) {
      const middle = Math.floor((left + right) / 2);

      if (nums[middle] === target) {
        result = middle;

        // 命中后仍要继续收缩,才能找到重复值的最左或最右索引。
        if (type === "first") {
          right = middle - 1;
        } else {
          left = middle + 1;
        }
      } else if (nums[middle] > target) {
        right = middle - 1;
      } else {
        left = middle + 1;
      }
    }

    return result;
  };

  return [main(nums, target, "first"), main(nums, target, "last")];
}

// 三组以上可直接运行的验证。
console.log(searchRange([5, 7, 7, 8, 8, 10], 8)); // 预期:[3, 4]
console.log(searchRange([5, 7, 7, 8, 8, 10], 6)); // 预期:[-1, -1]
console.log(searchRange([], 1)); // 预期:[-1, -1]
console.log(searchRange([2, 2, 2], 2)); // 预期:[0, 2]

时间复杂度:O(log n)。首次和末次位置各做一次二分查找,常数倍不改变复杂度。

空间复杂度:O(1)。只维护边界、当前索引与结果,没有随数组长度增长的辅助结构。

下载原始 JS 文件