题目:
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
解题思路
我们注意到旋转之后的数组实际上可以划分为两个排序的子数组,而且前面的子数组的元素大于或者等于后面子数组的元素。我们还注意到最小的元素刚好是这两个数组的分界线。在排序的数组中可以用二分查找实现O(logn)的查找。本题给出的数组在一定程度上是排序的,因此我们可以试着用二分查找法的思路来寻找这个最小的元素。
- 和二分查找法一样,我们用两个指针分别指向数组的第一个元素和最后一个元素。按照题目中旋转的原则,第一个元素应该是大于或者等于最后一个元素的;
- 接着我们可以找到数组中间的元素。如果中间元素位于前面的递增子数组,那么它应该大于或者等于第一个指针指向的元素。此时最小元素应该位于该中间元素之后,然后我们把第一个指针指向该中间元素,移动之后第一个指针仍然位于前面的递增子数组中;
- 同样,如果中间元素位于后面的递增子数组,那么它应该小于或者等于第二个指针指向的元素。此时最小元素应该位于该中间元素之前,然后我们把第二个指针指向该中间元素,移动之后第二个指针仍然位于后面的递增子数组中;
- 按照上述思路,第一个指针总是指向前面递增数组****的元素,第二个指针总是指向后面递增数组的元素。最终它们会指向两个相邻的元素**,而第二个指针指向的刚好是最小的元素,这就是循环结束的条件。
特殊情况:
1. 如果把排序数组的0个元素搬到最后面,这仍然是旋转数组,我们的代码需要支持这种情况。如果发现数组中的一个数字小于最后一个数字,就可以直接返回第一个数字了。
2. 下面这种情况,即第一个指针指向的数字、第二个指针指向的数字和中间的数字三者相等,我们无法判断中间的数字1是位于前面的递增子数组还是后面的递增子数组。这样的话,我们只能进行顺序查找。时间复杂度为O(n)。
C++实现
#include<iostream>
#include<vector>
using namespace std;
class Solution
{
public:
int Min(vector<int> numbers)
{
int index1 = 0;
int index2 = numbers.size()-1;
int indexMid = index1;
if (numbers.size() == 0)
throw new std::exception("Invalid");
while (numbers[index1]>=numbers[index2])
{
if (index1 == index2 - 1)
{
indexMid = index2; //indexMid赋予新值,因为上一轮的值被释放掉了
break;
}
int indexMid = (index1 + index2) / 2;
if (numbers[index1] == numbers[index2] && numbers[index1] == numbers[indexMid])
{
return MinInOrder(numbers);
}
if (numbers[indexMid] >= numbers[index1])
index1=indexMid ;
else
index2=indexMid ;
}
return numbers[indexMid]; //为了应对特殊情况,即把排序数组前面的0个元素搬到最后面
}
int MinInOrder(vector<int> numbers)
{
int result = numbers[0];
for (int i = 1; i < numbers.size(); i++)
{
if (result > numbers[i])
{
result = numbers[i];
}
}
return result;
}
};
python实现
使用二分查找的方法,我们将中轴元素numbers[pivot]与右边界元素numbers[high]进行比较,可能有以下的三种情况:
- 第一种情况是numbers[pivot]<numbers[high],这说明numbers[pivot]是最小值右侧的元素,我们可以忽略右半区间;
- 第二种情况是numbers[pivot]>numbers[high],这说明numbers[pivot]是最小值左侧的元素,我们可以忽略左半区间;
- 第三种情况是numbers[pivot]=numbers[high],如下图所示,我们并不能确定numbers[pivot]究竟在最小值的左侧还是右侧,因此我们不能莽撞地忽略某一部分的元素。我们唯一可以知道的是,由于它们的值相同,所以无论 numbers[high] 是不是最小值,都有一个它的「替代品」numbers[pivot],因此我们可以忽略二分查找区间的右端点。
class Solution:
def minArray(self, numbers: List[int]) -> int:
low, high = 0, len(numbers) - 1
while low < high:
pivot = low + (high - low) // 2
if numbers[pivot] < numbers[high]:
high = pivot
elif numbers[pivot] > numbers[high]:
low = pivot + 1
else:
high -= 1
return numbers[low]
- 时间复杂度:平均时间复杂度为 O(logn),其中 n 是数组numbers 的长度。如果数组是随机生成的,那么数组中包含相同元素的概率很低,在二分查找的过程中,大部分情况都会忽略一半的区间。而在最坏情况下,如果数组中的元素完全相同,那么while 循环就需要执行 n 次,每次忽略区间的右端点,时间复杂度为 O(n)。
- 空间复杂度:O(1)。