题目
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。
解析
其实这里考察的查找,而经典的查找不外乎有二分查找,二叉排序树,二叉平衡树,红黑树或者哈希查找之类的。所以我们遇到了查找问题,往这几个方向想准没错。
- 如果查找的数组是有序的或者部分有序的,考虑二分
- 如果要求O(1)查找到指定值,那么采用哈希思想即可。
而这道题是有序数组的变形,即为一个有序数组的变形。但是我们仍然可以尝试使用二分来做。仔细观察旋转数组会发现该数组有如下几个特点:
- 按照旋转的定义,原数组的最小的元素会被放到后面,由这个最小的元素可以把数组分成2部分,前一部分是非递减数组,而后一部分同样也是非递减数组。所以我们尝试按照二分查找的思想,用两个指针
low,high
分别指向数组的开头的和结尾,这样我们求出mid = low + (high - low) / 2
。 - 当
left <= mid
时,我们可知mid
是处于数组的第一部分,因为我们在第一条中已经说了数组分为了2个非递减数组。这时说明最小的元素不在[left,mid]
区间中,而是在[mid,right]
区间中,所以令left = mid
- 当
mid <= right
时,我们可知mid是处于数组的第二部分,因为我们在第一条中已经说了数组分为了2个非递减数组。这时说明最小的元素不在[mid,right]
区间中,而是在[left,mid]
区间中,所以令right = mid
- 正是上面的做法,才导致了left总是指向数组第一部分的最大值,而right总是指向数组第二部分的最小值。所以当
right - left == 1
时,即可结束查找,right指向就是我们所要求的最小值。
很容易写出如下代码:
public static int minNumberInRotateArray(int [] array) {
if(array.length == 0) {
return 0;
}
int low = 0;
int high = array.length - 1;
while(array[low] >= array[high]) {
if(high - low == 1) {
break;
}
mid = low + (high - low) / 2;
if(array[low] <= array[mid]) {
low = mid;
}else if(array[mid] <= array[high]) {
high = mid;
}
}
return array[high];
}
特例
虽然上诉算法很简单且有效,但是任何问题都有它自己的独特的大坑。我们忘了考虑两种特例。
- 数组就是原数组本身,旋转长度为0,这样最小的元素应该是low指向的元素啊
- 如果是如这种数组3,1,3,3,3。这种情况left == mid == right, 按照上面的算法,我们区间会缩为后半区间,其实不然,最小元素1在前半数组中。所以遇到这种情况,只能对[low,high]区间进行顺序查找。
public static int minNumberInRotateArray(int [] array) {
if(array.length == 0) {
return 0;
}
int low = 0;
int high = array.length - 1;
int mid = low;
while(array[low] >= array[high]) {
if(high - low == 1) {
mid = high;
break;
}
mid = low + (high - low) / 2;
/**
* 我知道剑指Offer中是让按顺序查找,那么这样复杂度不还是O(n)吗?
* 所以按照划分子问题的方法,根据mid划分2个子分组
* 然后对这两个子数组继续使用我们的二分即可呀!
* 采用了尾递归,不存在栈溢出,同时避免了顺序查找
* 优化:当然你可以令开一个函数,记录区间,避免我的数组复制的开销
* 我tm沙比了,这tm的复杂度飙到了O(nlogn)了
*/
// if(array[low] == array[mid] && array[mid] == array[high]) {
// int[] left = new int[mid - low + 1];
// int[] right = new int[high - mid + 1];
// System.arraycopy(array, low, left, 0, left.length);
// System.arraycopy(array, mid, right, 0, right.length);
// return Math.min(minNumberInRotateArray(left), minNumberInRotateArray(right));
// }
if(array[low] == array[mid] && array[mid] == array[high]) {
return ordinalGetMin(array, low, high);
}
if(array[low] <= array[mid]) {
low = mid;
}else if(array[mid] <= array[high]) {
high = mid;
}
}
return array[mid];
}
public static int ordinalGetMin(int[] array, int low, int high) {
// int min = Integer.MAX_VALUE;
// for(int i = low; i <= high; i++) {
// min = Math.min(min, array[i]);
// }
// return min;
for(int i = low + 1; i <= high; i++) {
if(array[i] < array[i - 1]) {
return array[i];
}
}
return array[low];
}