您的位置:首页 > 职场人生

《剑指Offer》学习笔记--面试题8:旋转数组的最小数字

2015-05-06 15:55 387 查看
题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序得数组的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2}和{1,2,3,4,5}的一个旋转,该数组的最小值为1。

这道题的直观解法并不难,从头到尾遍历数组一次,我们就能找出最小的元素。这种思路的时间复杂度显然是O(n)。但是这个思路没有利用旋转数组的特性,肯定达不到面试官的要求。

我们注意到旋转之后的数组实际上可以划分为两个排序得子数组,而且前面的子数组的元素都大于或者等于后面子数组的元素。我们还注意到最小元素刚好是这两个子数组的分界线。在排序的数组中我们可以用二分查找法实现O(logn)的查找。本题给出的数组在一定程度上是排序的,因此我们可以试着用二分查找法的思路来寻找这个最小元素。

和二分查找法一样,我们用两个指针分别指向数组的第一个元素和最后一个元素。按照题目汇总旋转的规则,第一个元素应该是大于或者等于最后一个元素的(这其实并不完全对,还有特例,后面再讨论)。

接着我们可以找到数组中间的元素。如果该中间元素位于前面的递增子数组,那么它应该大于或者等于第一个指针指向的元素。此时数组中最小的元素应该位于中间元素的后面。我们可以把第一个指针指向该中间元素,这样可以缩小范围。移动之后的第一个指针仍然位于前面的递增子数组中。

同样,如果中间元素位于后面的递增子数组,那么它应该小于或者等于第二个指针指向的元素。此时该数组中最小的元素应该位于该中间元素的前面。我们可以把第二个指针指向该中间元素,这样也可以缩小寻找的范围。移动之后的第二个指针仍然位于后面的递增子数组之中。

不管是移动第一个指针还是第二个指针,查找的范围都会缩小到原来的一半。接下来我们再使用更新之后的两个指针,重复做新一轮的查找。

按照上述的思路,第一个指针总是指向前面递增数组的元素,而第二个指针总是指向后面递增数组的元素。最终第一个指针将指向前面子数组的最后一个元素,而第二个指针会指向后面子数组的第一个元素。也就是说他们最终会指向两个相邻的元素,而第二个指针恰好指向最小的元素,这就是循环结束的条件。

基于这个思路,我们可以写出如下代码:

int Min(int* numbers, int length)
{
if(numbers == NULL || length <= 0){
throw new std::exception("Invalid parameters");
}

int index1 = 0;
int index2 = length - 1;
int indexMid = index1;
while(numbers[index1] >= numbers[index2]){
if(index2 - index1 == 1){
indexMid = index2;
break;
}

indexMid = (index1 + index2)/2;
if(numbers[indexMid] >= numbers[index1]){
index1 = indexMid;
}
else if(numbers[indexMid] <= numbers[index2]){
index2 = indexMid;
}
}
return numbers[indexMid];
}
前面我们提到在旋转数组中,由于是把递增数组前面的若干个数字搬到数组的后面,因此第一个数字总是大于或者等于最后一个数字。但按照定义还有一个特例:如果把排序数组的前面的0个元素搬到最后面,即排序数组的本身,这仍然是数组的一个旋转,我们的代码需要支持这种情况。此时,数组中的第一个数字就是最小的数字,可以直接返回。这就是在上面的代码中把indexMid初始化为index1的原因。一旦发现数组中第一个数字小于最后一个数字,表明该数组是排序的,就可以直接返回第一个数字了。

上述代码是否就完美了呢?面试官会告诉我们其实不然。他将提示我们再仔细分析小标为index1和index2(index1和index2分别和图中P1和P2相对应)的两个数相同的情况。在前面的代码中,当这两个数相同,并且它们中间的数字也相同,我们把indexMid赋值给了index1,也就是认为此事最小的数字位于中间数字的后面。是不是一定这样呢?

我们来看一个例子。数组{1,0,1,1,1}和数组{1,1,1,0,1}都可以看成递增排序数组{0,1,1,1,1}的旋转。

在这两种情况中,第一个指针和第二个指针指向的数字都是1,并且两个指针中间的数字也是1,这3个数字相同。在第一种情况中,中间数字(下标为2)位于后面的子数组;在第二章情况中,中间数字(下标为2)位于前面的子数组中。因此,当两个指针指向的数字及它们中间的数字三者相同的时候,我们无法判断中间数字是位于前面的子数组中还是后面东风子数组中,我们就无法判断了。

在把这个问题分析清楚形成清晰的思路之后,我们就可以把前面的代码修改为:

#include <iostream>
using namespace std;

int MinInorder(int *numbers, int index1, int index2)
{
int res = numbers[index1];
for(int i = index1 + 1; i <= index2; i++){
if(numbers[i] < res){
res = numbers[i];
}
}
return res;
}
int Min(int* numbers, int length)
{
if(numbers == NULL || length <= 0){
throw new std::exception("Invalid parameters");
}

int index1 = 0;
int index2 = length - 1;
int indexMid = index1;
while(numbers[index1] >= numbers[index2]){
if(index2 - index1 == 1){
indexMid = index2;
break;
}

indexMid = (index1 + index2)/2;
//如果下标为index1、index2和indexMid指向的三个数字相等,
//则只能顺序查找
if(numbers[index1] == numbers[indexMid] &&
numbers[indexMid] == numbers[index2]){
return MinInorder(numbers, index1, index2);
}
if(numbers[indexMid] >= numbers[index1]){
index1 = indexMid;
}
else if(numbers[indexMid] <= numbers[index2]){
index2 = indexMid;
}
}
return numbers[indexMid];
}

int main()
{
int a[] = {3,4,5,1,2};
int length = sizeof(a)/sizeof(int);
cout<<Min(a, length)<<endl;
system("pause");
return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: