题目描述
给你一个按照非递减顺序排列的整数数组 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]
解题思路总览
| 方法 | 核心思想 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|---|
| 二分查找(左边界 + 右边界) | 分别找 target 的左边界和右边界 | O(log n) | O(1) | 标准解法,最常考 |
| 二分查找(lower_bound 变形) | 利用 lower_bound 找左边界,再减1找右边界 | O(log n) | O(1) | 代码简洁 |
| 直接遍历(不推荐) | 线性扫描数组 | O(n) | O(1) | 不满足题目要求,仅作对比 |
方法一:二分查找(左边界 + 右边界)
代码实现
cpp
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if (nums.empty()) return {-1, -1};
// 找左边界(第一个 >= target 的位置)
int start = findLowerBound(nums, target);
// 检查左边界是否有效
if (start == nums.size() || nums[start] != target) {
return {-1, -1};
}
// 找右边界(第一个 > target 的位置,然后减1)
int end = findLowerBound(nums, target + 1) - 1;
return {start, end};
}
private:
int findLowerBound(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
}
};
核心思想
本题本质是查找 target 的左边界和右边界:
- 左边界:第一个 >= target 的位置
- 右边界:第一个 > target 的位置减1
利用二分查找的变体,通过 nums[mid] >= target 来收缩右边界,逐步逼近左边界。
算法流程图
以 nums = [5,7,7,8,8,10], target = 8 为例:
第一步:找左边界(第一个 >= 8 的位置)
初始:left=0, right=5
第1轮:mid=2, nums[2]=7 < 8,left=mid+1=3
第2轮:mid=4, nums[4]=8 >= 8,right=mid-1=3
第3轮:mid=3, nums[3]=8 >= 8,right=mid-1=2
left=3, right=2,循环结束
返回 left = 3(左边界)
第二步:找右边界(第一个 > 8 的位置减1)
找 > 8,实际上就是找第一个 >= 9 的位置
调用 findLowerBound(nums, 9)
初始:left=0, right=5
第1轮:mid=2, nums[2]=7 < 9,left=mid+1=3
第2轮:mid=4, nums[4]=8 < 9,left=mid+1=5
第3轮:mid=5, nums[5]=10 >= 9,right=mid-1=4
left=5, right=4,循环结束
返回 left = 5,然后 end = 5 - 1 = 4(右边界)
结果:[3, 4]
逐行解析
cpp
if (nums.empty()) return {-1, -1};
处理空数组边界情况,直接返回 -1, -1。
cpp
int start = findLowerBound(nums, target);
调用辅助函数找到 target 的左边界(第一个 >= target 的位置)。
cpp
if (start == nums.size() || nums[start] != target) {
return {-1, -1};
}
两种情况说明 target 不存在于数组中:
- start == nums.size():说明所有元素都小于 target
- numsstart != target:说明 start 位置的值大于 target(因为 start 是第一个 >= target 的位置)
cpp
int end = findLowerBound(nums, target + 1) - 1;
找右边界:第一个 > target 的位置 = 第一个 >= target+1 的位置,减 1 得到右边界。
为什么要用 target+1?因为数组是整数,可以通过找 target+1 的左边界来间接找到 > target 的位置。
cpp
int findLowerBound(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] >= target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
}
找左边界(第一个 >= target 的位置)的核心逻辑:
- numsmid >= target:说明左边界在 left, mid 区间(包括 mid),更新 right = mid - 1
- numsmid < target:说明左边界在 mid+1, right 区间,更新 left = mid + 1
循环结束后,left 就是第一个 >= target 的位置。
复杂度分析
| 复杂度 | 分析 |
|---|---|
| 时间 | 两次二分查找,每次 O(log n),总共 O(log n) |
| 空间 | O(1),只使用了常数个变量 |
方法二:直接使用 lower_bound(STL 思想)
代码实现
cpp
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if (nums.empty()) return {-1, -1};
// 使用 STL lower_bound 找左边界
auto lower_it = lower_bound(nums.begin(), nums.end(), target);
// 检查左边界是否有效
if (lower_it == nums.end() || *lower_it != target) {
return {-1, -1};
}
// 使用 STL upper_bound 找右边界
auto upper_it = upper_bound(nums.begin(), nums.end(), target);
int start = lower_it - nums.begin();
int end = upper_it - nums.begin() - 1;
return {start, end};
}
};
核心思想
直接利用 C++ STL 的两个标准库函数:
lower_bound:返回第一个 >= target 的迭代器upper_bound:返回第一个 > target 的迭代器
STL 原理简析
lower_bound 的实现逻辑与手动实现完全相同:
cpp
template<class ForwardIt, class T>
ForwardIt lower_bound(ForwardIt first, ForwardIt last, const T& value) {
ForwardIt it;
while (first < last) {
it = first + (last - first) / 2;
if (*it < value) {
first = it + 1;
} else {
last = it;
}
}
return first;
}
复杂度分析
| 复杂度 | 分析 |
|---|---|
| 时间 | O(log n) |
| 空间 | O(1) |
方法三:手动实现 lower_bound 和 upper_bound
代码实现
cpp
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if (nums.empty()) return {-1, -1};
int start = myLowerBound(nums, target);
if (start == nums.size() || nums[start] != target) {
return {-1, -1};
}
int end = myUpperBound(nums, target) - 1;
return {start, end};
}
private:
int myLowerBound(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
int myUpperBound(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return left;
}
};
myLowerBound vs myUpperBound 对比
| 对比项 | myLowerBound | myUpperBound |
|---|---|---|
| 目标 | 第一个 >= target | 第一个 > target |
| 条件 | numsmid < target | numsmid <= target |
| 等于时的动作 | right = mid - 1 | left = mid + 1 |
算法流程图
myLowerBound(nums, target):
目标:找第一个 >= target 的位置
条件:nums[mid] < target -> left = mid + 1
否则 -> right = mid - 1
myUpperBound(nums, target):
目标:找第一个 > target 的位置
条件:nums[mid] <= target -> left = mid + 1
否则 -> right = mid - 1
以 nums = [5,7,7,8,8,10], target = 8 为例:
myLowerBound(nums, 8):
找第一个 >= 8,返回 3
myUpperBound(nums, 8):
找第一个 > 8,返回 5
end = 5 - 1 = 4
结果:[3, 4]
复杂度分析
| 复杂度 | 分析 |
|---|---|
| 时间 | O(log n) |
| 空间 | O(1) |
边界情况分析
情况1:数组为空
输入: nums = [], target = 0
分析: nums.empty() 为 true,直接 return {-1, -1}
结果: [-1, -1]
情况2:target 小于所有元素
输入: nums = [5,7,7,8,8,10], target = 3
分析: 找左边界 findLowerBound(nums, 3)
逐步向右移动,最终 left = 0
但 nums[0] = 5 != 3
结果: [-1, -1]
情况3:target 大于所有元素
输入: nums = [5,7,7,8,8,10], target = 100
分析: 找左边界 findLowerBound(nums, 100)
逐步向左移动,最终 left = 6 = nums.size()
start == nums.size()
结果: [-1, -1]
情况4:target 只出现一次
输入: nums = [5,7,7,8,8,10], target = 10
分析: 左边界:找到 10 在位置 5
右边界:找第一个 > 10,即第一个 >= 11
找不到,upper = 6,end = 6 - 1 = 5
结果: [5, 5]
情况5:target 充满整个数组
输入: nums = [8,8,8,8], target = 8
分析: 左边界:findLowerBound(nums, 8) = 0
右边界:findLowerBound(nums, 9) - 1 = 4 - 1 = 3
结果: [0, 3]
情况6:target 不存在但夹在两个数之间
输入: nums = [5,7,7,8,8,10], target = 9
分析: 左边界:findLowerBound(nums, 9)
第1轮:mid=2, nums[2]=7 < 9,left=3
第2轮:mid=4, nums[4]=8 < 9,left=5
第3轮:mid=5, nums[5]=10 >= 9,right=4
left=5, right=4,循环结束
返回 left = 5,但 nums[5] = 10 != 9
结果: [-1, -1]
面试追问 FAQ
| 问题 | 回答 |
|---|---|
| 为什么要用 target+1 来找右边界? | 数组是整数,可以通过找 target+1 的左边界来间接找到 > target 的位置。这是避免单独写 upper_bound 的技巧 |
| left <= right 和 left < right 有什么区别? | left <= right 最终 left > right;left < right 最终 left == right。两者都可用于二分,区别在于初始边界设置 |
| 如果数组中有重复元素怎么办? | 两种方法都能正确处理:方法一用 target+1 间接找右边界;方法三直接用 upper_bound |
| 如何处理负数 target? | target+1 可能溢出(如果 target = INT_MAX)。解决方案:直接用 upper_bound 或改用 target + 1LL |
| 二分查找的常见陷阱有哪些? | 1. 整数溢出:用 mid = left + (right - left) / 2 代替 mid = (left + right) / 2;2. 循环终止条件:left <= right 还是 left < right;3. 边界更新:left = mid + 1 还是 left = mid |
| 如果要求时间复杂度为 O(log n),为什么不能遍历? | 遍历最坏情况 O(n),不满足要求。二分查找每次将搜索区间缩小一半,保证 O(log n) |
| 如何在 O(log n) 内同时找左边界和右边界? | 思路一:两次二分(分别找左边界和右边界);思路二:一次二分找到任意一个 target,再向左右扩展(最坏 O(n)) |
相关题目
| 题目 | 难度 | 核心区别 |
|---|---|---|
| 34. 在排序数组中查找元素的第一个和最后一个位置(本题) | 困难 | 找左边界和右边界 |
| 35. 搜索插入位置 | 简单 | 找插入位置(相当于左边界) |
| 74. 搜索二维矩阵 | 中等 | 二维矩阵的二分查找 |
| 704. 二分查找 | 简单 | 标准二分查找 |
| 278. 第一个错误的版本 | 简单 | 二分查找的简单应用 |
| 162. 寻找峰值 | 中等 | 二分查找找峰值 |
总结
| 要点 | 说明 |
|---|---|
| 核心思想 | 利用数组有序,通过二分查找找左边界(第一个 >= target)和右边界(第一个 > target) |
| 找左边界 | 第一个 >= target 的位置 |
| 找右边界 | 第一个 > target 的位置 = 第一个 >= target+1 的位置减 1 |
| 防溢出 | 使用 mid = left + (right - left) / 2 |
| 时间复杂度 | O(log n) |
| 空间复杂度 | O(1) |
| 关键技巧 | 用 target+1 间接找右边界,避免单独处理 |