题目描述:
给你一个按照非递减顺序排列的整数数组
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 的位置
-
右边界:最后一个等于 target 的位置
可以分别用两次二分查找:
-
找左边界 :用二分查找,找第一个
>= target的位置 -
找右边界 :用二分查找,找第一个
> target的位置,再减1
具体过程示例:
nums = [5, 7, 7, 8, 8, 10], target = 8
找左边界(第一个 >= 8):
left=0, right=5, mid=2, nums[2]=7 < 8 → left=3
left=3, right=5, mid=4, nums[4]=8 >= 8 → right=3
left=3, right=3, mid=3, nums[3]=8 >= 8 → right=2
left=3 > right=2,结束,左边界 = left = 3 ✅
找右边界(第一个 > 8):
left=0, right=5, mid=2, nums[2]=7 <= 8 → left=3
left=3, right=5, mid=4, nums[4]=8 <= 8 → left=5
left=5, right=5, mid=5, nums[5]=10 > 8 → right=4
left=5 > right=4,结束,右边界 = left - 1 = 4 ✅
代码实现:
cpp
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int left = lowerBound(nums, target);
int right = upperBound(nums, target) - 1;
// 检查是否找到
if (left <= right && right < nums.size() && nums[left] == target) {
return {left, right};
}
return {-1, -1};
}
private:
// 找第一个 >= target 的位置
int lowerBound(vector<int>& nums, int target) {
int left = 0, right = nums.size();
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
// 找第一个 > target 的位置
int upperBound(vector<int>& nums, int target) {
int left = 0, right = nums.size();
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] <= target) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
}
};
更简洁的写法(用 STL):
cpp
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
auto left = lower_bound(nums.begin(), nums.end(), target);
auto right = upper_bound(nums.begin(), nums.end(), target);
if (left == nums.end() || *left != target) {
return {-1, -1};
}
return {(int)(left - nums.begin()), (int)(right - nums.begin() - 1)};
}
};
复杂度分析:
| 维度 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(log n) | 两次二分查找 |
| 空间复杂度 | O(1) | 只用常数个变量 |
关键细节:
1. 为什么用 left < right 而不是 left <= right?
这是左闭右开区间的二分模板:
cpp
int left = 0, right = nums.size(); // 注意 right 是 size,不是 size-1
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) left = mid + 1;
else right = mid;
}
return left;
-
right初始为size,表示开区间 -
循环结束时
left == right,就是答案
2. 为什么 upperBound 返回 left 后要减1?
upperBound 返回的是第一个 > target 的位置 ,所以右边界是 upperBound - 1。
3. 如何判断是否找到?
cpp
if (left <= right && right < nums.size() && nums[left] == target)
-
left <= right:确保区间有效 -
right < nums.size():确保不越界 -
nums[left] == target:确保真的找到了
4. 和「搜索旋转排序数组」的区别
| 题目 | 区别 |
|---|---|
| 33. 搜索旋转排序数组 | 旋转数组,找 target |
| 34. 查找第一个和最后一个位置 | 有序数组,找 target 的边界 |
总结:
| 要点 | 说明 |
|---|---|
| 核心思想 | 两次二分查找:找左边界和右边界 |
| 左边界 | 第一个 >= target 的位置 |
| 右边界 | 第一个 > target 的位置减1 |
| 时间复杂度 | O(log n) |
| 空间复杂度 | O(1) |