二分查找的经典应用:在排序数组中查找元素的第一个和最后一个位置

0 阅读4分钟

问题描述

给定一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。要求找出目标值在数组中的开始位置和结束位置。如果数组中不存在目标值,返回 [-1, -1]

时间复杂度要求O(log n)

示例

text

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

输入:nums = [], target = 0
输出:[-1,-1]

解题思路

题目中的“非递减顺序” + “O(log n)” 已经明确暗示我们使用二分查找。但普通的二分查找只能找到一个目标值的位置,无法直接得到第一个和最后一个。

因此,我们可以将问题拆解为:

  1. 找到目标值在数组中的第一个位置(左边界)
  2. 找到目标值在数组中的最后一个位置(右边界)

两次二分查找,分别控制查找方向:

  • 找左边界:当 nums[mid] == target 时,不立即返回,而是继续向左收缩右边界(right = mid - 1),直到锁定最左边的目标。
  • 找右边界:当 nums[mid] == target 时,继续向右收缩左边界(left = mid + 1),直到锁定最右边的目标。

这样,两次二分查找的时间复杂度均为 O(log n),整体仍为 O(log n)

代码实现(JavaScript)

javascript

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var searchRange = function(nums, target) {
    let left = 0;
    let right = nums.length - 1;
    let leftIndex = -1;
    let rightIndex = -1;

    // 1. 找左边界(第一个位置)
    while (left <= right) {
        let mid = Math.floor((left + right) / 2);
        if (nums[mid] === target) {
            leftIndex = mid;
            right = mid - 1;   // 继续向左搜索
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }

    // 重置左右指针
    left = 0;
    right = nums.length - 1;

    // 2. 找右边界(最后一个位置)
    while (left <= right) {
        let mid = Math.floor((left + right) / 2);
        if (nums[mid] === target) {
            rightIndex = mid;
            left = mid + 1;    // 继续向右搜索
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }

    return [leftIndex, rightIndex];
};

代码详解

1. 寻找左边界

  • 初始化 left = 0right = nums.length - 1
  • 当 nums[mid] === target 时,记录当前位置 leftIndex = mid,并设置 right = mid - 1,强制在左半区继续查找,确保找到最左边的目标。
  • 其他情况按照标准二分查找更新指针。

2. 寻找右边界

  • 同样重置左右指针。
  • 当 nums[mid] === target 时,记录 rightIndex = mid,并设置 left = mid + 1,强制在右半区继续查找,确保找到最右边的目标。

3. 边界情况

  • 数组为空:left > right 直接退出循环,返回 [-1, -1]
  • 目标值不存在:两次查找中都不会记录有效索引,最终返回 [-1, -1]

复杂度分析

  • 时间复杂度O(log n),两次二分查找,每次都是对数级别。
  • 空间复杂度O(1),只使用了常数个变量。

测试用例验证

输入输出
[5,7,7,8,8,10]8[3,4]
[5,7,7,8,8,10]6[-1,-1]
[]0[-1,-1]
[1]1[0,0]
[2,2,2,2,2]2[0,4]

常见误区 & 进阶思考

误区1:找到任意一个 target 后向两边线性扩展

有些同学可能会想到先用二分找到任意一个 target,然后向左右线性扫描。在最坏情况下(全数组都是 target),线性扩展会使时间复杂度退化为 O(n),不满足题目要求。

误区2:边界更新条件写错

例如在找左边界时,不小心写成 left = mid + 1,会导致永远找不到最左值;或者循环条件忘记等号,导致遗漏边界元素。

进阶思考:能否用一次二分查找同时获得左右边界?

理论上可以,但需要额外处理边界逻辑,代码会变得复杂且易错。两次独立的二分查找思路清晰、不易出错,是工程上推荐的做法。

小结

这道题是二分查找的进阶应用,核心在于当找到目标时不立即停止,而是继续向一侧收缩,从而定位边界。掌握这种“二分查找 + 边界收缩”的思想,对解决很多有序数组中的查找问题(如寻找插入位置、统计目标出现次数等)都非常有帮助。

希望这篇博客能帮你彻底理解这道经典题目。如果对二分查找还不太熟悉,建议先练练基础版《704. 二分查找》,再回过头来体会边界的精妙控制。

Happy Coding! 🚀