题目描述(困难难度)
153 题 升级版,依旧是旋转过的数组,一个排序好的数组,把前边的若干的个数,一起移动到末尾,找出最小的数字。只不过这道题中的数字可能有重复的。
思路分析
想一下 153 题 我们怎么做的。
mid
和 end
比较。
mid
< end
:最小值在左半部分。
mid
> end
:最小值在右半部分。
所以我们只需要把 mid
和 end
比较,mid < end
丢弃右半部分(更新 end = mid
),mid > end
丢弃左半部分(更新 start = mid + 1
)。直到 end
等于 start
时候结束就可以了。
之前没有重复的数字,所以 mid
要么大于 end
,要么小于 end
。这里的话,就需要考虑如果 mid == end
的话,我们该怎么做。代码框架也就出来了。
public int findMin(int[] nums) {
int start = 0;
int end = nums.length - 1;
while (start < end) {
int mid = (start + end) >>> 1;
if (nums[mid] > nums[end]) {
start = mid + 1;
} else if (nums[mid] < nums[end]) {
end = mid;
} else {
//添加代码
}
}
return nums[start];
}
可以想几个例子考虑下。
3 3 1 2 3 3 3 3 3
^ ^
mid end
上边的情况, mid == end,此时最小值在 mid 左边
3 3 3 3 3 2 3
^ ^
mid end
上边的情况, mid == end,此时最小值在 mid 右边
2 3 3 1 1 1 1 1 1
^ ^
mid end
上边的情况, mid == end,此时最小值在 mid 左边
当相等的时候,最小值可能在左边,也可能在右边,问题的关键就是去解决这个问题。
想法一
我想到的一种解决方案是如果遇到了相等的情况,将 mid
指针不停的向左滑动,直到当前的值不再等于 end
。然后就会有三种情况。
情况一,移动结束的值小于了
end
,此时最小值一定在mid
的左边3 3 1 2 3 3 3 3 3 ^ ^ mid end
情况二,移动结束的值大于了
end
,此时最小值一定是结束位置的后一个值2 3 3 1 1 1 1 1 1 ^ ^ mid end
情况三,移动到头后依旧是等于
end
,此时最小值一定在mid
的右边3 3 3 3 3 2 3 ^ ^ mid end
大于小于等于也只有上边的三种情况了,所以代码也就出来了。
public int findMin(int[] nums) {
int start = 0;
int end = nums.length - 1;
while (start < end) {
int mid = (start + end) >>> 1;
if (nums[mid] > nums[end]) {
start = mid + 1;
} else if (nums[mid] < nums[end]) {
end = mid;
} else {
int temp = mid;
//不停前移
while (temp >= 0) {
if (nums[temp] == nums[end]) {
temp--;
//情况一
} else if (nums[temp] < nums[end]) {
end = mid;
break;
//情况二
} else {
return nums[temp + 1];
}
}
//情况三
if (temp == -1) {
start = mid + 1;
}
}
}
return nums[start];
}
想法二
参考 这里 ,非常简洁优雅。
主要思想就是把问题转换回 153 题 ,同样也是解决相等的时候怎么办。
只做一件事,end--
。因为 mid
和 end
相等,所以我们直接把 end
抛弃一定不会影响结果的。
public int findMin(int[] nums) {
int start = 0;
int end = nums.length - 1;
while (start < end) {
int mid = (start + end) >>> 1;
if (nums[mid] > nums[end]) {
start = mid + 1;
} else if (nums[mid] < nums[end]) {
end = mid;
} else {
end--;
}
}
return nums[start];
}
想法三
参考 这里 。
首先根据题目的意思,数组是被分成了两段。
当有重复数字的时候,之所以麻烦,就是因为有一个重复数字可能会被切割在两段里,比如下边的例子。
3 3 1 2 3 3 3 3 3
^ ^
mid end
分成了 3 3 和 1 2 3 3 3 3 两段,3 同时处于两段
3 3 3 3 3 2 3
^ ^
mid end
分成了 3 3 3 3 和 2 3 两段,3 同时处于两段
而如果所有重复的数字只处于一段里,其实并不复杂,比如下边的例子
1 2 3 3 3 3
^ ^
mid end
只有一段 1 2 3 3 3 3
7 7 6 6 6
^ ^
mid end
分成了 7 7 和 6 6 6 两段,没有数字处于两段里
对于上边的情况,其实我们可以确定,当 mid
和 end
相等的时候,最小值一定在 start
到 mid
之间。也就是和 mid < end
的情况一致的。
所以我们可以做一个预处理,保证所有重复数字不在两段里出现即可,再简单化,也就是保证切割的位置不要是重复数字。也就是比较 start
和 end
是否相同,相同的话 end--
即可。
while (nums[end] == nums[start] && end > start) {
end--;
}
最后只要把 153 题 的代码拿过来即可。
public int findMin(int[] nums) {
int start = 0;
int end = nums.length - 1;
while (nums[end] == nums[start] && end > start) {
end--;
}
while (start < end) {
int mid = (start + end) >>> 1;
if (nums[mid] > nums[end]) {
start = mid + 1;
} else{
end = mid;
}
}
return nums[start];
}
总
思路一主要是对新出现的情况进行细分来解决问题,思路二和思路三是把问题向原先的问题靠拢,从而尽可能少的变动了代码。不过这道题由于一些情况很特殊,所以虽然用了二分,最坏时间复杂度也降到了 O(n)
,不再是 log
级的。