给你一个按照非递减顺序排列的整数数组 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^9nums是一个非递减数组-10^9 <= target <= 10^9
思路一
二分法。不过恶心的是要找一个区间,遇到相等的情况,要着重思考。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if (nums.empty()) return {-1,-1};
if (nums.size() == 1) {
if (target != nums[0]) return {-1,-1};
return {0,0};
}
// 是否贪心往最小索引搜索
auto bin_search = [&nums, target](int b, int e, bool min) {
while (b + 1 < e) {
int m = b + (e - b) / 2;
// printf("min%d: b%d, m%d, e%d\n", min, b, m , e);
if (target == nums[m]) {
if (min) {
if (e != m + 1) e = m + 1;
// 最后剩3个数的时候,要小心处理边界,因为边界更新后可能和上一次一模一样,造成死循环
else return nums[b] == target ? b : m;
} else {
b = m;
}
} else if (target < nums[m]) {
e = m;
} else {
b = m;
}
}
if (nums[b] == target) return b;
return -1;
};
// n >= 2
const int n = nums.size();
int beg = bin_search(0, n, true);
int end = bin_search(0, n, false);
return {beg, end};
}
};思路二:利用 lower_bound 语义
C++ 泛型算法中提供了 lower_bound 和 upper_bound,其含义为
- lower_bound: 找到序列中不小于(大于等于)目标值的第一个索引
- upper_bound:找到序列中大于目标值的第一个索引
题目让我们找值为 target 的区间。其实就是找 lower_bound 和 upper_bound 呀。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
auto start = lower_bound(nums.begin(), nums.end(), target);
// lowerbound在界外,或lowerbound处不等于target,肯定是大于target。
// 那么整个数组中不可能存在target了,可以直接返回。
if (start == nums.end() || *start != target) {
return {-1, -1};
}
// auto end = upper_bound(nums.begin(), nums.end(), target);
// or
auto end = lower_bound(nums.begin(), nums.end(), target + 1);
return {
static_cast<int>(start - nums.begin()),
static_cast<int>(end - nums.begin() - 1)
};
}
};手写 lowerbound
lowerbound 说起来也就是个二分。参考灵神的二分法视频教程,写出三种二分区间。
Key Insight
不要像传统二分那样去找答案在哪,而是要把区间之外的元素想清楚。
以闭区间为例,定义区间 [L, R],我们想要知道区间里面的元素和 target 之间的大小关系。准确说,我们是要知道区间外的元素和 target 之间的大小关系。区间内的元素总是还未访问的。取区间中点 M,如果 ,我们立刻知道
- 若
A[M] < target,则[L, M]内的元素都小于 target. 把 L 更新为 M + 1. - 否则,
[M, R]内的元素都大于等于 target. 把 R 更新为 M - 1. - 这样一来,新区间
[L, R]内的元素是未访问过的,我们继续重复上面的步骤,直到区间内没有元素,即我们直到了所有元素和 target 的大小关系。
最终,区间收缩为空。由于我们维护的规则,任意一次循环结束时,总有
A[L-1] < targetA[R+1] >= target
我们可以称之为循环不变量。因此,我们要找的索引就是 R+1. 又因为退出循环时 L = R + 1,所以也可以将 L 作为答案。
学会这一手,让你的二分不再死循环!
// 左闭右闭
int lowerbound0(vector<int>& nums, int target) {
int l = 0, r = nums.size() - 1;
/*维护 L, R 使得
1. A[L-1] < target
2. A[R+1] >= target
*/
while (l <= r) { // 区间有元素
int mid = l + (r - l) / 2;
if (nums[mid] < target) {
// 此时我知道 mid 及其左边都 < target(染红色)
// 但 [mid+1, r] 内的元素还未确定大小,
// 所以 l 更新为 mid+1.
l = mid + 1;
} else {
// 此时我知道 mid 及其右边都满足 >= target(染蓝色)
// 但是我们区间的定义是“还未和target确定大小关系的集合”,
// 而非包含最终答案的集合!
// 而这个区间应该是 [l, mid - 1],所以 r 更新为 mid-1.
r = mid - 1;
}
} // 目标是 [l,r] 之外均染色,[l,r] 之内均未染色。直到 [l,r] 为空,所有元素都染色。
return l;
}
// 左闭右开
int lowerbound1(vector<int>& nums, int target) {
int l = 0, r = nums.size();
/*维护 L, R 使得
1. A[L-1] < target
2. A[R] >= target
*/
while (l < r) {
int mid = l + (r - l) / 2;
if (nums[mid] < target) {
l = mid + 1;
} else {
r = mid;
}
}
return l;
}
// 左开右开
int lowerbound1(vector<int>& nums, int target) {
int l = -1, r = nums.size();
/*维护 L, R 使得
1. A[L] < target
2. A[R] >= target
*/
while (l + 1 < r) {
int mid = l + (r - l) / 2;
if (nums[mid] < target) {
l = mid;
} else {
r = mid;
}
}
return r;
}然后套用思路二即可。
lower_bound 转换
如果题目不是让你找第一个大于等于,而是大于/小于/小于等于呢?对于整数数组来说,这些是可以互相转换的。假设我们已经有了一个 lowerbound 函数返回第一个大于等于 x 的索引。那么对于,
> x === lower_bound(x + 1)< x === lower_bound(x) - 1<= x ==== lower_bound(x+1) - 1
Reference
强烈建议观看灵神的二分法讲解视频!