题目8:旋转数组的最小数字(leetcode链接:https://leetcode-cn.com/problems/xuan-zhuan-shu-zu-de-zui-xiao-shu-zi-lcof/)
问题分析
本题最直观简单的解法就是暴力法,将数组遍历一遍找出数组中的最小值返回。但是,这种做法时间复杂度较高,还可以继续优化。
旋转数组可以有以下几种情况,我们逐个分析
1)将最开始的n个数搬到数组的末尾,且n > 0 && n < numsSize(即旋转后的数组与原数组不同)
我们注意到旋转之后的数组实际上可以划分为两个排序的子数组,而且前面的子数组的元素都大于或者等于后面子数组的元素。我们还注意到最小的元素刚好是这两个子数组的分界线。在排序的数组中我们可以用二分查找法实现O(logn)的查找。本题给出的数组在一定程度上是排序的,因此我们可以试着用二分查找法的思路来寻找这个最小的元素。
和二分查找法一样,我们用两个指针分别指向数组的第一个元素和最后一个元素。按照题目中旋转的规则,第一个元素应该是大于或者等于最后一个元素的(这其实不完全对,还有特例,后面再加以讨论)。接着我们可以找到数组中间的元素。如果该中间元素位于前面的递增子数组,那么它应该大于或者等于第一个指针指向的元素。此时数组中最小的元素应该位于该中间元素的后面。我们可以把第一个指针指向该中间元素,这样可以缩小寻找的范围。移动之后的第一个指针仍然位于前面的递增子数组之中。
同样,如果中间元素位于后面的递增子数组,那么它应该小于或者等于第二个指针指向的元素。此时该数组中最小的元素应该位于该中间元素的前面。我们可以把第二个指针指向该中间元素,这样也可以缩小寻找的范围。移动之后的第二个指针仍然位于后面的递增子数组之中。
不管是移动第一个指针还是第二个指针,查找范围都会缩小到原来的一半。接下来我们再用更新之后的两个指针,重复做新一轮的查找。按照上述的思路,第一个指针总是指向前面递增数组的元素,而第二个指针总是指向后面递增数组的元素。最终第一个指针将指向前面子数组的最后一个元素,而第二个指针会指向后面子数组的第一个元素。也就是它们最终会指向两个相邻的元素,而第二个指针指向的刚好是最小的元素。这就是循环结束的条件。
2)第一种情况中,我们是把递增数组的前n个数移到数组后边,因此旋转后的数组中第一个数肯定大于等于最后一个元素。如果旋转后的数组和原数组相等,即将数组的前面的0个数移到后边,那么最后一个元素一定大于等于第一个元素。因此程序中我们还需要判断最后一个元素是否会大于最后一个元素,如果是我们就直接返回第一个元素。
3)基于上面两种情况,我们还需要考虑,如果数组元素全部相等(begin mid end的值相等)就无法使用二分查找判断,这是我们就需要遍历数组查找。
代码描述
int Min(int* nums, int numsSize)
{
//定义三个指针
int begin = 0, end = numsSize - 1;
int mid = begin;//这里让mid = begin,防止数组有序需要返回第一个元素
//如果数组元素有序(最后一个元素大于第一个元素),就直接返回第一个元素
while (nums[begin] >= nums[end])
{
//如果两个指针相等,就可以返回后边指针的值了
if (end - begin == 1)
{
mid = end; //要保证,数组元素有序返回mid值,因此要让mid == end
break;
}
mid = (begin+end) / 2;
//如果三个数相等,只能顺序查找
if (nums[begin] == nums[end] && nums[begin] == nums[mid])
{
int result = nums[begin];
for (int i = 0; i <= end; i++)
{
if (result > nums[i])
return nums[i];
}
//如果数组中的所有元素都相等,则result不可能大于nums[i],这时我们返回数组中的最后一个元素即可
return nums[end];
}
//判断最小元素在数组中的位置
else if (nums[begin] <= nums[mid])
begin = mid;
else if (nums[end] >= nums[mid])
end = mid;
}
return nums[mid];
}