跳到正文
第 34 题中等

在排序数组中查找元素的第一个和最后一个位置

Find First and Last Position of Element in Sorted Array

在非递减数组里找出 target 第一次和最后一次出现的下标,找不到返回 [-1, -1]。

在力扣中练习

题目描述

给你一个按照非递减顺序排列的整数数组 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

解题思路

主解法

两次二分 · 分别找左右边界

命中之后不停手,继续往左/往右压缩区间

普通二分命中 target 就结束,得到的只是某一次出现的位置。要找一整段重复值的端点,需要把“命中后往哪边继续找”也纳入规则。

自拟主例 nums = [1,2,2,4,4,4,4,7,9],target = 4,返回 [3,6]。第一次命中位于重复段内部,不能直接当作任一端点。

分步图解

同一个有序数组执行两次二分,命中后还要继续向目标方向收缩。

找最左 · 闭区间 [left,right]
0
1
2
3
4
5
6
7
8
1
2
2
4
4
4
4
7
9
已确认的左右端点
-1-1
1 / 4
命中只代表找到了一个位置,暂存后继续缩小区间。

找最左时,命中先暂存 mid,再把右端改为 mid-1;找最右时同样暂存,但把左端改为 mid+1。值太小、太大时分别丢弃不可能包含答案的半边。

两次查找都使用闭区间 [left,right],所以循环条件是 left <= right。退出后暂存值才是最终端点。若第一次始终没有命中,直接返回 [-1,-1]。

时间 O(log n)空间 O(1)

易错点

  • 命中后直接 return,只拿到某个 target 的下标,不是边界。
  • 找右边界时把 left = mid + 1 和 right = mid - 1 写反,区间朝反方向收缩。
  • 闭区间模板却把循环写成 left < right:只剩一个候选点时直接退出,会漏掉答案。
对照解法

STL · lower_bound + upper_bound

用现成的「第一个 ≥ x」和「第一个 > x」拼出区间

同样是 O(log n) 的库函数写法;重点是区分第一个不小于与第一个大于 target 的位置。

C++ 的 lower_bound / upper_bound 做的正是上面两次二分:

  • lower_bound(begin, end, target):第一个 ≥ target 的位置;
  • upper_bound(begin, end, target):第一个 > target 的位置。

于是答案区间就是 [lower_bound, upper_bound)。两者相等说明一个 target 都没有,返回 {-1, -1};否则左端点是 first - begin,右端点是 last - begin - 1。

注意 upper_bound 返回的是第一个大于 target 的位置,所以右边界要减一。这个「左闭右开」的表示是 STL 的通用约定,想清楚之后比手写边界更不容易错。

面试时如果允许用 STL,这是最稳的写法;如果要求手写,记住上面那两句「第一个 ≥ x」「第一个 > x」,模板可以现推。

  1. 1lower_bound 找到第一个 ≥ target 的位置。
  2. 2upper_bound 找到第一个 > target 的位置。
  3. 3两者相等说明不存在,返回 {-1, -1}。
  4. 4左端点取 first,右端点取 last - 1。
时间 O(log n)空间 O(1)

易错点

  • upper_bound 的结果忘记减一,右端点会落在 target 后面一格。
  • 用 first == nums.end() 判不存在:target 比所有元素都大时 first 才是 end,判不完整。

代码与演示

主解法配有图解;对照解法用于比较思路、复杂度或实现方式。

演示输入nums = [1,2,2,4,4,4,4,7,9], target = 4

同一个有序数组执行两次二分,命中后还要继续向目标方向收缩。

找最左 · 闭区间 [left,right]
0
1
2
3
4
5
6
7
8
1
2
2
4
4
4
4
7
9
已确认的左右端点
-1-1
1 / 9
查看 C++ 代码

延伸阅读

  • 「第一个 ≥ x」和「第一个 > x」是二分的两个原子操作,几乎所有边界问题都能由它们组合出来。
  • 手写二分时坚持只用一种区间表示(左闭右开 [left, right) 或闭区间 [left, right]),边界错误大多来自混用。
  • 如果数组是降序,把比较符号整体反过来即可,思路完全不变。