剑指 Offer 11. 旋转数组的最小数字
WA,寄了
l++ 和 r–并不是o(logn)复杂度,而是(n),比如 10 折半直接折掉了 5 个数,而 l++, r-- 只能折 2 个数,悟了悟了,大家看个乐呵就行
起因
l 为左端点, r 为右端点
我刚拿到这道题也懵了,排序数组得用二分查找来收缩查找区间啊,可是旋转之后咋用二分来收缩区间呢?
于是广快去题解区拜读大佬们的文章,于是又又又又看到了笑脸路飞大佬的文章 ,写的那是相当不错啊,尤其是关于用mid与右端点做比较才能收缩区间的讲解
。
不过当numbers[r]==numbers[mid]
时只能退化到线性查找让我觉得有点不甘心,在我仔细研究之后,我发现原来能用numbers[l] 和 numbers[r]
来进一步收缩区间。
numbers[l] 和 numbers[r] 的特点
如果旋转了
如:3, 4, 5, 1, 2
-
numbers[l] 一定大于等于 numbers[r]
-
numbers[l] 是左排序数组的最小值,numbers[r] 是右排序数组的最大值
如果没有旋转
- numbers[l] 一定小于 numbers[r]
利用到了特点的代码
最小值一定在右排序数组,把 l 移出左排序数组, 把 r 往更小的元素索引位置移动就行了
与右端点比较
class Solution {
public int minArray(int[] numbers) {
int l, r;
l = 0;
r = numbers.length - 1;
while(l < r) {
if(numbers[l] < numbers[r]) {
// 说明[l, r]没旋转,因此我们直接返回最小值左端点即可
return numbers[l];
}
int mid = l + (r - l) / 2;
if(numbers[mid] < numbers[r]) {
// [mid, r] 为右排序数组 r 已经位于右排序数组了,只需要把 r 往更小的元素移动就行了
r = mid;
} else if(numbers[mid] > numbers[r]) {
// 此时 [l, mid] 位于左排序数组把 l 移出左排序数组
l = mid + 1;
} else if(numbers[mid] == numbers[r]) {
if(numbers[l] == numbers[r]) {
/* 当 numbers[l]、numbers[r]、numbers[mid] 三值相等,此时我们无法确定最小值在 (l, mid) 还是在 (mid, r) ,
因此 l 和 r 指针同时向中间靠拢收缩查找区间
为什么可以直接移动 l 和 r指针呢?
因为此时 l,mid,r 必然位于两个或3个不同的索引上,不可能为1个,如果是一个,那么l==r, 就退出循环了
要么是 [2, ?, 2, ?, 2] 要么是 [2, 2]
因此收缩区间可以直接变为 [l+1, r-1],就相当于去掉了两个 2 ,咱们还剩一个 mid 的 2,
所以不用担心这样移动会把最小值移出查找区间(万一 2 为最小值的情况)
*/
l++;
r--;
} else if(numbers[l] > numbers[r]) {
/* 这说明咱们的数组肯定经过了旋转
注意此时 numbers[mid] == numbers[r]
因为 numbers[r] 是右排序数组的最大值,所以 [mid, r] 必然是重复的元素
因此收缩 r, 但是别收缩的太猛,万一这个重复元素就是最小值呢。
所以 r=mid 就够了(因为我们条件是 l==r 就可以退出循环,然后返回 numbers[l] 所以不管是 l 还是 r 都可以取到这个最小值)
*/
r = mid;
}
}
}
return numbers[l];
}
}
与左端点比较
难点就是有重复的元素,如果我们能去重,那自然也能用左端点来收缩区间
class Solution {
public int minArray(int[] numbers) {
int l, r;
l = 0;
r = numbers.length - 1;
while(l < r) {
if(numbers[l] < numbers[r]) {
// 说明[l, r]未旋转
return numbers[l];
}
int mid = l + (r - l) / 2;
if(numbers[l] > numbers[mid]) {
// 说明 [mid, r] 是右排序数组,因此移动 r 到更小的 mid 处
r = mid;
}else if(numbers[l] < numbers[mid]) {
// 说明 [l, mid] 是左排序数组,因此把 l 移出已经判断为左排序数组的区间(即[l, mid])
l = mid + 1;
}else if(numbers[l] == numbers[mid]) {
if(numbers[l] == numbers[r]) {
l++;
r--;
}else if(numbers[l] > numbers[r]) {
/* 此时 numbers[l] == numbers[mid] , 并且 numbers[l] > numbers[r] 说明数组经过了旋转
那么 [l, mid] 肯定是重复的元素
如 [2, 2, 7, 1, 1] , 此时即使我们移动 l 到 mid+1 的位置也不会把最小值移出查找区间(mid + 1 可能在左排序数组,也可能在右排序数组了)
*/
l = mid + 1;
}
}
}
return numbers[l];
}
}
回答一些问题
为什么 while 循环条件是 l < r ?
因为返回结果是 numbers[l]
, 而我们 r 初始值为 numbers.length - 1,万一这个 r 刚好位于最小值上呢
。
你要是循环条件为 l <= r,也就是说 l == r + 1 时, 才能退出循环,岂不是一开始就把人家排除了。
彩蛋
hahaha,我居然吃透了一道 hard 题