题目:有一个长度为 n 的非降序数组,比如[1,2,3,4,5],将它进行旋转,即把一个数组最开始的若干个元素搬到数组的末尾,变成一个旋转数组,比如变成了[3,4,5,1,2],或者[4,5,1,2,3]这样的。请问,给定这样一个旋转数组,求数组中的最小值。
这道题最直观的解法并不难,从头到尾遍历数组一次,就能找到最小的元素。这种思路的时间复杂度太高。而且没有利用输入的旋转数组的特性。
通过观察我们可以发现旋转之后的数组实际上可以划分为两个排序的子数组,而且前面的子数组的元素都大于或者等于后面子数组的元素。并且最小元素刚好是这两个子数组的分界线。这样我们可以使用二分查找来实现。
接着我们找出数组中间的元素,用两个指针分别指向数组的第一个元素和最后一个元素。
如果中间元素位于前面的递增子数组,那么它应该大于或者等于第一个指针指向的元素。此时该数组中最小的元素应该位于该中间元素的后面。我们把第一个指针指向该中间元素,这就缩小了寻找范围。移动后第一个指针仍然位于前面的子数组中。
同理,如果中间元素位于后面的递增子数组,那么它应该小于或者等于第二个指针指向的元素。此时该数组中最小的元素应该位于该中间元素的前面。我们把第二个指针指向该中间元素,这也就缩小了寻找范围。移动后第二个指针仍然位于后面的子数组中。
不管移动第一个指针还是第二个指针,查找范围都会缩小到原来的一半。接下来用更新之后的两个指针,重新做新一轮的查找。
按照上面的思路,第一个指针最终会只想前面子数组的最后一个元素,而第二个指针最终会指向后面子数组的第一个元素,而第二个指针指向的刚好是最小的元素,循环结束。
画图举例:
代码如下:
int minNumberInRotateArray(int* rotateArray, int rotateArrayLen)
{
int left = 0;
int right = rotateArrayLen - 1;
int mid = left; //如果把顺序数组前面的0个元素搬到后面,还是数组本身,这时数组中第一个元素就是最小的数字。所以这里这样初始化。
while (rotateArray[left] >= rotateArray[right])
{
if (right - left == 1) //判断数组中元素为2个的情况
{
mid = right;
break;
}
mid = (left + right) / 2;
if (rotateArray[mid] >= rotateArray[left])
left = mid;
else if (rotateArray[mid] <= rotateArray[right])
right = mid;
}
return rotateArray[mid];
}
然而,上面的代码并没有完美解决问题。我们看一个例子。
数组{1,0,1,1,1}和数组{1,1,1,0,1}都可以看成递增数组{0,1,1,1,1}的旋转,这两种情况下,第一个指针和第二个指针指向的数字都是1,中间的数字也是1。因此,当两个指针指向的数字及它们中间的数字三者相同的时候,我们无法判断中间的数字是位于前面的子数组还是后面的子数组,我们就只能用顺序查找的方法。
代码修改如下:
int minNumberInRotateArray(int* rotateArray, int rotateArrayLen)
{
int left = 0;
int right = rotateArrayLen - 1;
int mid = left;
while (rotateArray[left] >= rotateArray[right])
{
if (right - left == 1)
{
mid = right;
break;
}
mid = (left + right) / 2;
if (rotateArray[left] == rotateArray[right] && rotateArray[mid] == rotateArray[left])
return order(rotateArray, left, right);
if (rotateArray[mid] >= rotateArray[left])
left = mid;
else if (rotateArray[mid] <= rotateArray[right])
right = mid;
}
return rotateArray[mid];
}
int order(int* rotateArray, int left, int right)
{
int result = rotateArray[left];
for (int i = left + 1; i <= right; i++)
{
if (result > rotateArray[i])
result = rotateArray[i];
}
return result;
}
还有一种二分查找的方法,思路跟代码都比较简单。
使用中间值与右端进行比较。
1. 中间大于右边 [3, 4, 5, 1, 2],这种情况下,最小数一定在右边;则left = middle + 1。
2. 中间等于右边 [1, 0, 1, 1, 1], 这个是[0, 1, 1, 1, 1] 旋转过来的,这时候需要缩小范围 right--;,这里不能是left++,因为是非降序数组,所以要缩小右边范围,把较小值向右推,符合我们的判断规则。
3. 中间小于右边 [5, 1, 2, 3, 4], 这种情况下,最小数字则在左半边;则right = middle。
代码如下:
int minNumberInRotateArray(int* rotateArray, int rotateArrayLen)
{
if (rotateArrayLen == 0) return 0;
int left = 0, right = rotateArrayLen - 1, mid;
if (rotateArray[right] > rotateArray[left]) return rotateArray[0];
while (left < right) {
mid = left + (right - left) / 2;
if (rotateArray[mid] > rotateArray[right]) left = mid + 1;
else if (rotateArray[mid] == rotateArray[right]) right--;
else right = mid;
}
return rotateArray[left];
}