-
题目:
找出一个旋转数组numbers的最小值;
有重复元素;
-
思路:
1.二分: 时间O(log N) : 在最坏情况下会退化到 O(N)例如 [1, 1, 1, 1],就是因为本题有重复元素,只能将通常情况下优化为logn;空间O(1) : 变量使用常数大小的额外空间;
虽数组整体并不是有序的,但前后两部分都是递增的,因此仍可以用二分;
二分的核心:根据每次numbers [ mid ] 的值,必须确定目标值(即本题的最小值)在mid的哪一侧;
我们只关注整个旋转数组中的最小元素和最后一个元素numbers[ r ]:最小值右侧的元素都<=x,最小值左侧的元素>=x。因此若numbers [ mid ] > numbers [ r ] ,则mid一定在最小值左侧;若numbers [ mid ] < numbers [ r ] ,则mid一定在最小值右侧;若numbers [ mid ] == numbers [ r ] ,则mid在最小值的左右两侧都有可能(因为本题有重复元素),但二分必须要不断缩小区间,可以只把 right 舍弃掉,因为并不影响结果;
class Solution {
public:
int minArray(vector<int>& numbers) {
if (numbers.empty()) return INT32_MIN; //判空
int len = numbers.size();
if (numbers[0] < numbers[len - 1]) return numbers[0]; //未旋转
int l = 0, r = len - 1;
//最坏情况[2,2,2,2,2],每次都进入r--分支,相当于线性遍历
while (l <= r) {
int mid = l + ((r - l) >> 2);
if (numbers[mid] > numbers[r]) l = mid + 1; //mid在最小值左侧,可以把mid左侧舍弃
else if (numbers[mid] < numbers[r]) r = mid; //mid在最小值右侧,可以把mid右侧舍弃
else r--; //mid不确定在最小值哪一侧,但为了缩小区间,可以把r所在的位置舍弃
}
//如果循环条件为l < r,则出循环时一定l==r,因此return numbers[l]和numsber[r]均可
//如果循环条件为l <= r,则循环的最后一步由于r--导致l>r,此时最小值在下标l上
return numbers[l];
}
};
2.yxc的题解
class Solution {
public:
int minArray(vector<int>& numbers) {
if (numbers.empty()) return -1;
int n = numbers.size() - 1;
if (numbers[0] < numbers[n]) return numbers[0];//未旋转
while (n > 0 && numbers[0] == numbers[n]) --n; //把后半部分末尾的等于numebrs[0]的值去掉,以便二分
if (numbers[0] < numbers[n]) return numbers[0];
int l = 0, r = n;//二分去找右半部分中,第一个<numbers[0]的数就是最小值;
while (l < r) {
int mid = l + ((r - l) >> 1);
if (numbers[mid] >= numbers[0]) l = mid + 1;
else r = mid;
}
return numbers[l];
}
};
- 总结:
二分尽管道理简单,实现真的很容易错:l < r这个地方,单层循环中更新l和r时,返回l处还是r处的值,都好容易错,每道题的case都不同需要考虑清楚;
mid和r比较而不是l的原因:若每次将 nums[ mid ] 与 nums[ left ]比较,无法确定最小值在mid左边还是右边:例:[3, 4, 5, 1, 2] 或 [1, 2, 3, 4, 5] ;每次将nums[ mid ] 与 nums[ right ]比较,可以确定最小值所在的目标区间:例:[1, 2, 3, 4, 5] 或 [3, 4, 5, 1, 2] 或 [2, 3, 4, 5 ,1];
方法2中第一个while处容易漏写n>0,注意不是n>=0
需要再刷;