【中等】34.在排序数组中查找元素的第一个和最后一个位置

0 阅读3分钟

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

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

示例 2:

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

示例 3:

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

提示:

  • 0 <= nums.length <= 105
  • -109 <= nums[i] <= 109
  • nums 是一个非递减数组
  • -109 <= target <= 109

1. 生活案例:在学生名册里找“姓张的”范围

想象你手里有一份按姓氏拼音排好序的学生名册,里面有一群姓“张”的同学。

  • 任务:老板想知道姓“张”的同学从第几个人开始,到第几个人结束。

  • 过程

    1. 你翻开名册中间,发现是个姓“张”的。
    2. 找起点:虽然你找到了一个姓张的,但你不知道他是不是第一个。所以你先拿个本子记下他的位置,然后继续往名册的左边翻,看看前面还有没有姓张的。
    3. 找终点:同样的,你记下那个位置后,再往名册的右边翻,看看后面还有没有姓张的。

最后,你记下的两个最极端的“张”的位置,就是我们要的区间。


2. 代码实现与详细注释

这是你图片中的代码,我为你添加了详细的中文注释。这段代码复用了同一个二分查找函数 findBound,通过一个开关 isFirst 来控制是找左边界还是右边界。

JavaScript

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
var searchRange = function(nums, target) {
    // 这是一个辅助函数:用来找“边界”
    // isFirst 为 true 找第一个位置,false 找最后一个位置
    let findBound = (isFirst) => {
        let left = 0;
        let right = nums.length - 1;
        let bound = -1; // 记录找到的最新边界

        while (left <= right) {
            let mid = Math.floor(left + (right - left) / 2);

            if (nums[mid] === target) {
                // 【核心逻辑】:找到了目标,先存起来
                bound = mid;
                
                if (isFirst) {
                    // 如果我们要找第一个,那就继续往左边切 (收缩右边界)
                    right = mid - 1;
                } else {
                    // 如果我们要找最后一个,那就继续往右边切 (收缩左边界)
                    left = mid + 1;
                }
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return bound;
    }

    // 调用两次:一次找头,一次找尾
    let first = findBound(true);
    let last = findBound(false);
    
    return [first, last];
};

3. 核心原理解析

为什么不直接用 indexOflastIndexOf

虽然 JavaScript 自带这两个方法,但在最坏情况下(比如数组里全是 8),它们的效率是 O(n)O(n)。题目明确要求 O(logn)O(\log n) ,这就逼着我们必须使用二分查找来跳跃式搜索。

关键点:找到 target 后的“贪婪”搜索

正常的二分查找在 nums[mid] === target 时就直接返回了。但这道题我们需要:

  • 找左边界:即便找到了,也要假设左边可能还有,所以 right = mid - 1
  • 找右边界:即便找到了,也要假设右边可能还有,所以 left = mid + 1

这种思路被称为“二分查找左/右侧边界”,是处理重复元素排序数组的标准套路。

复杂度分析

  • 时间复杂度O(logn)O(\log n)。我们执行了两遍二分查找,总时间仍然是对数级别的。
  • 空间复杂度O(1)O(1)。只使用了常数个指针变量。

总结

这道题教会我们:二分查找不仅能找“存在”,还能找“范围” 。只要在相等时调整左右指针的收缩方向,就能像挤牙膏一样挤出目标元素的精准边界。