热题100 - 34. 在排序数组中查找元素的第一个和最后一个位置

85 阅读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 <= 10^5
  • -10^9 <= nums[i] <= 10^9
  • nums 是一个非递减数组
  • -10^9 <= target <= 10^9

思路:

在了解标准二分查找的思想之后,这个题就好做了。为什么非要了解标准的二分查找呢?因为这类问题中,数组的下标是非常容易混淆的。通常来讲,有两种方式来处理数组下标,一种是 [left, right], 另一种是[left, right)。这两种方式的left都赋值为0,但是right的值会分别赋值为length - 1length

第一次是找目标值是否存在。若存在,最左边在哪里。区别于之前的二分查找,找到了就返回。这里找到了之后,把找到的地方设置为上界(注意是开区间),然后继续二分查找。这种写法当循环退出的时候,左右是相等的。左边的值就是连续值下界(这是我用小例子验证过的,比如 8 8 8 8 8找8)。类似的,如果把中间值+1赋值给左侧,就会找到上界。

实现:

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int al = 0, ar = nums.length;  // open
        boolean found = false;
        int[] res = new int[]{-1, -1};
        while(al < ar) {
            int mid = (al + ar) >> 1;
            if (target == nums[mid]) {
                // used to be return
                found = true;
                ar = mid;
            } else if (target > nums[mid]) {
                al = mid + 1;
            } else {
                ar = mid;  // still open
            }
        }
        // al is the pos to insert.. 
        if (!found) {
            return res;
        }
        res[0] = al;
        int bl = 0, br = nums.length;  // open
        while(bl < br) {
            int mid = (bl + br) >> 1;
            if (target == nums[mid]) {
                bl = mid + 1;
            } else if (target > nums[mid]) {
                bl = mid + 1;
            } else {
                br = mid;  // still open
            }
        }
        res[1] = bl-1;
        return res;
    }
}

DS批注:

这段代码的目标是找到有序数组中目标值的起始和结束位置。其思路是使用两次二分查找分别确定左右边界,确保时间复杂度为对数级别。

思路分析

  1. 第一次二分查找(左边界): • 初始化左指针 al=0 和右指针 ar=nums.length(左闭右开区间)。 • 当 al < ar 时,计算中间点 mid。若 nums[mid] == target,则缩小右边界 ar = mid 继续向左搜索;若 nums[mid] < target,则调整左边界 al = mid + 1;否则调整右边界 ar = mid。 • 循环结束后,若找到目标值(foundtrue),al 即为左边界;否则直接返回 [-1, -1]

  2. 第二次二分查找(右边界): • 初始化左指针 bl=0 和右指针 br=nums.length。 • 当 bl < br 时,计算中间点 mid。若 nums[mid] == target,则调整左边界 bl = mid + 1 继续向右搜索;若 nums[mid] < target,同样调整左边界 bl = mid + 1;否则缩小右边界 br = mid。 • 循环结束后,右边界为 bl - 1,因为 bl 会停在第一个大于 target 的位置。

时间复杂度

O(log n):两次独立的二分查找,每次查找的时间复杂度为 O(log n),总复杂度为 O(2 log n) → 简化为 O(log n)。

空间复杂度

O(1):仅使用了固定数量的额外变量,结果数组为必要返回空间,不计入额外空间复杂度。

代码正确性

左边界查找:通过不断向左收缩确保找到第一个出现的位置。 • 右边界查找:通过向右扩展后回退一位,定位到最后一个出现的位置。 • 处理了目标值不存在的情况(found 标志位判断)。

该方法高效且简洁,适用于有序数组中的范围搜索问题。