给定一个按照升序排列的整数数组 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]
分析:
由于nums数组是已经排序过的,无需排序这个步骤。显而易见最容易的解决方式就是顺序遍历,找到最前和最后的两个位置。但是这样的方式非常不稳定,如果target在nums中靠前且相同值的数个数少将会非常快,反之则有可能非常慢,但是最慢不过O(n),可见慢的时候和快的时候差别十分明显。
#pragma region 暴力法,顺序遍历
/*
执行结果:通过
执行用时:28 ms, 在所有 C++ 提交中击败了16.11%的用户
内存消耗:13.8 MB, 在所有 C++ 提交中击败了13.11%的用户
最快的一次:
执行用时: 12 ms
内存消耗: 13.6 MB
*/
vector<int> searchRange(vector<int>& nums, int target) {
if (nums.size() == 0)
return vector<int>({ -1,-1 });
int LIndex, RIndex;
vector<int> retVec;
bool Lfind = false, Rfind = false;
for (int i = 0; i < nums.size(); ++i) {
if (!Lfind) {
if (nums[i] == target) {
Lfind = true;
LIndex = i;
}
}
else {
if (nums[i] != target) {
Rfind = true;
RIndex = i - 1;
break;
}
}
}
if (Lfind) {
retVec.push_back(LIndex);
if (Rfind)
retVec.push_back(RIndex);
else
retVec.push_back(nums.size() - 1);
}else {
retVec.push_back(-1);
retVec.push_back(-1);
}
return retVec;
}
#pragma endregion
有没有办法让这个遍历的过程快一点呢?我们可以从头部尾部一起向中间逼近,这样循环次数减少了一半,而且对于target偏向nums某一边这种情况的处理也更加理想。不过还是很慢,甚至失去了第一种方法在极端情况下的超快速度。
#pragma region 暴力法,两边遍历
/*
执行结果:通过
执行用时:28 ms, 在所有 C++ 提交中击败了16.11%的用户
内存消耗:13.7 MB, 在所有 C++ 提交中击败了17.00%的用户
*/
vector<int> searchRange(vector<int>& nums, int target) {
if (nums.size() == 0)
return vector<int>({ -1,-1 });
int LIndex, RIndex;
vector<int> retVec;
bool Lfind = false, Rfind = false;
int i = 0,j = nums.size()-1;
while(i<=j) {
// cout << "before(i = " << i << ",j = " << j << ")" << endl;
if (!Lfind) {
if (nums[i] == target) {
Lfind = true;
LIndex = i;
}else
++i;
}
if (!Rfind) {
if (nums[j] == target) {
Rfind = true;
RIndex = j;
}else
--j;
}
// cout << "after(i = " << i << ",j = " << j << ")" << endl;
if (Lfind && Rfind)
break;
}
if (Lfind) {
retVec.push_back(LIndex);
retVec.push_back(RIndex);
}
else {
retVec.push_back(-1);
retVec.push_back(-1);
}
return retVec;
}
#pragma endregion
以上两种方法对于target在nums中部的情况束手无策,因此联想到可以利用二分法快速找到某个等于target的点,并以它为中心向两侧扩散。
#pragma region 二分查值并从中心扩散
/*
执行结果:通过
执行用时:24 ms, 在所有 C++ 提交中击败了42.54%的用户
内存消耗:13.8 MB, 在所有 C++ 提交中击败了6.65%的用户
*/
vector<int> searchRange(vector<int>& nums, int target) {
//二分法找值并从中间扩散法
if (nums.size() == 0)
return vector<int>({-1,-1});
vector<int> retVec;
bool isFind = false;
int MaxIndex = nums.size() - 1;
int LIndex = 0;
int RIndex = nums.size() - 1;
int Index = (LIndex+RIndex) / 2;//开始位置的下标
while(LIndex <= RIndex) {//查找等于target的点
if (target == nums[Index]) {
isFind = true;
break;
}
else if (target < nums[Index]) {
RIndex = Index - 1;
Index = (LIndex+RIndex) / 2;
}
else {
LIndex = Index + 1;
Index = (LIndex + RIndex) / 2;
}
}
if (!isFind) {
retVec.push_back(-1);
retVec.push_back(-1);
}
else {
// cout << "Find Index is --(" << Index << ")" << endl;
bool lend = false, rend = false;
for (int i = Index,j = Index;; i--, j++) {
if (i >= 0 && nums[i] == target) {
LIndex = i;
}
else { lend = true; }
if (j <= MaxIndex && nums[j] == target) {
RIndex = j;
}
else { rend = true; }
if (lend && rend) {
break;
}
}
retVec.push_back(LIndex);
retVec.push_back(RIndex);
}
return retVec;
}
#pragma endregion
这样一来相比前两种方法稳定快捷了许多。但还不够,进阶中要求时间复杂度O(logn),看到logn不由得联想到二分法,我们可以分别利用二分法找到等于target的值的左右边界LIndex和RIndex,不过在判断二分条件时与通常的大于等于小于有所不同,对于LIndex的查找过程,我们可以利用类似下面的判断条件。
if (nums[LIndex] < target) {//目标值在右侧
}else if (nums[LIndex] == target && (LIndex == 0 || nums[LIndex-1] < target)) {//找到目标值
}else{}
因为等于target的值中序号最小的那一个元素的前面要么没有元素(序号为0),要么它的前一个元素小于target。
代码实现如下:
#pragma region 双二分(分开循环)
/*
执行结果:通过
执行用时:12 ms, 在所有 C++ 提交中击败了98.53%的用户
内存消耗:13.7 MB, 在所有 C++ 提交中击败了17.84%的用户
执行结果:通过
执行用时:16 ms, 在所有 C++ 提交中击败了92.37%的用户
内存消耗:13.6 MB, 在所有 C++ 提交中击败了50.85%的用户
*/
vector<int> searchRange(vector<int>& nums, int target) {//双二分(分开循环)
if (nums.size() == 0)
return vector<int>({ -1,-1 });
vector<int> retVec;
int LLowIndex = 0, RLowIndex = 0;
int LHighIndex = nums.size() - 1, RHighIndex = nums.size() - 1;
int LIndex = (LLowIndex + LHighIndex) / 2;
int RIndex = (RLowIndex + RHighIndex) / 2;
bool Lfind = false, Rfind = false;
while (LLowIndex <= LHighIndex) {
if (nums[LIndex] < target) {//目标值在右侧
LLowIndex = LIndex + 1;
LIndex = (LLowIndex + LHighIndex) / 2;
}
else if (nums[LIndex] == target && (LIndex == 0 || nums[LIndex-1] < target)) {//找到目标值
Lfind = true;
break;
}
else {//目标值在左侧
LHighIndex = LIndex - 1;
LIndex = (LLowIndex + LHighIndex) / 2;
}
}
while (RLowIndex <= RHighIndex) {
if (nums[RIndex] > target) {//目标值在左侧
RHighIndex = RIndex - 1;
RIndex = (RLowIndex + RHighIndex) / 2;
}
else if (nums[RIndex] == target && (RIndex == nums.size()-1 || nums[RIndex + 1] > target)) {//找到目标值
Rfind = true;
break;
}
else {//目标值在右侧
RLowIndex = RIndex + 1;
RIndex = (RLowIndex + RHighIndex) / 2;
}
}
if (Lfind) {
retVec.push_back(LIndex);
retVec.push_back(RIndex);
}
else {
retVec.push_back(-1);
retVec.push_back(-1);
}
return retVec;
}
#pragma endregion
可以看到这个时候速度已经很快了,而且比较稳定,对于各种姿势的nums都能很好地驾驭。那么,可不可以更快呢?🙀,上面我们用了两个while循环来分别寻找LIndex和RIndex,下面用一个循环来试试。
#pragma region 双二分(合并循环)
/*
执行结果:通过
执行用时:4 ms, 在所有 C++ 提交中击败了99.99%的用户
内存消耗:13.8 MB, 在所有 C++ 提交中击败了10.24%的用户
最慢的一次
执行用时: 32 ms
内存消耗: 13.8 MB
*/
vector<int> searchRange(vector<int>& nums, int target) {//双二分(合并循环)
if (nums.size() == 0)
return vector<int>({ -1,-1 });
vector<int> retVec;
int LLowIndex = 0, RLowIndex = 0;
int LHighIndex = nums.size() - 1, RHighIndex = nums.size() - 1;
int LIndex = (LLowIndex + LHighIndex) / 2;
int RIndex = (RLowIndex + RHighIndex) / 2;
bool Lfind = false, Rfind = false;
while (LLowIndex <= LHighIndex || RLowIndex <= RHighIndex) {
if (Lfind && Rfind)
break;
if (!Lfind) {
if (nums[LIndex] < target) {//目标值在右侧
LLowIndex = LIndex + 1;
LIndex = (LLowIndex + LHighIndex) / 2;
}
else if (nums[LIndex] == target && (LIndex == 0 || nums[LIndex - 1] < target)) {//找到目标值
Lfind = true;
}
else {//目标值在左侧
LHighIndex = LIndex - 1;
LIndex = (LLowIndex + LHighIndex) / 2;
}
}
if (!Rfind) {
if (nums[RIndex] > target) {//目标值在左侧
RHighIndex = RIndex - 1;
RIndex = (RLowIndex + RHighIndex) / 2;
}
else if (nums[RIndex] == target && (RIndex == nums.size() - 1 || nums[RIndex + 1] > target)) {//找到目标值
Rfind = true;
}
else {//目标值在右侧
RLowIndex = RIndex + 1;
RIndex = (RLowIndex + RHighIndex) / 2;
}
}
}
if (Lfind) {
retVec.push_back(LIndex);
retVec.push_back(RIndex);
}else {
retVec.push_back(-1);
retVec.push_back(-1);
}
return retVec;
}
#pragma endregion
芜湖🛫!