剑指Offer_#11_旋转数组的最小数字

剑指Offer_#11_旋转数组的最小数字

Contents

题目

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。  

示例 1:

输入:[3,4,5,1,2]
输出:1
示例 2:

输入:[2,2,2,0,1]
输出:0

思路分析

旋转操作将原本的一个有序数组分割为两个有序数组,分别称之为左有序数组和右有序数组。
所谓的最小数字其实就是旋转点的数字,或者说右有序数组的第一个数字,其下标用pivot表示。

整体的思路是二分查找,使用两个指针,通过比较,不断地缩小范围,最后两个指针相遇,指向的就是要找的最小数字。

二分查找

初始化左右指针为数组的开头(i)和结尾(j),然后取中间点(m),判断中间点是位于左有序数组,还是右有序数组,判断条件是:

  • numbers[m] > numbers[j]
    • 说明中间点m位于左有序数组。要找的pivot在当前m的右边,所以把左边界右移,即执行i = m + 1。这里加1是因为,number[m]大于某个数字(number[m]或者number[j]),所以m绝不可能是最小数字,直接排除掉。
  • numbers[m] < numbers[j]
    • 说明中间点m位于右有序数组。要找的pivot在当前m的左边,所以把右边界左移,即执行j = m。这里没有像上边一样减一是因为,pivot是一定属于右有序数组的,如果减一,可能就错过pivot数字了。
  • 特殊情况:numbers[m] == numbers[j]
    • 不好判断中间点m到底是位于左有序数组还是右有序数组,这里就可以直接左移右边界一位,即执行j--。为什么可以这么做,是因为这样做并不会错过pivot,将pivot排除在外。具体论证如下。

分情况讨论: m位于左有序数组,m位于右有序数组。

  1. 若m位于左有序数组,那么pivot在m的右边。继续分类讨论:j就是pivot,j不是pivot.
    a) j就是pivot,这时j--就把pivot错过了,直接将范围缩小到左有序数组。举例说明:在[1,1,1,2,1]当中,numbers[m] == numbers[j],pivot与j重合。这隐含了一个信息,也就是numbers[j]<=numbers[i]<=numbers[m](将pivot还原到数组开头,得到的数组是有序数组),又有numbers[m]==numbers[j],则说明numbers[i]==numbers[i+1]==...==numbers[m]==numbers[j]。那么之后一定是将j逐渐左移,进入到[i,m]范围内,最终定位到的值也一定与pivot相等,不会错过pivot。
    b) j不是pivot,pivot又在m右边,那执行j--正好是在缩小范围,也不会错过pivot.
  2. 若m位于右有序数组,那么pivot在m的左边。应该左移右边界j,直接j--是不会错过pivot的。之后继续进行二分搜索,必然是把范围逐渐左移。

为什么选择numbers[j]作为判断标准? 原因是numbers[j]一定位于右有序数组中,但numbers[i]不一定位于左有序数组中,例如数组[1,2,3,4,5]只有右有续数组,pivot==0
以下举两种不同测试用例进行分析。

  • 不存在相等数字

不存在相等数字
不存在相等数字

这时,左有序数组的任何一个数大于右边界数(即numbers[j])。
右有序数组的任何一个数小于右边界数。

  • 存在相等数字

存在相等数字
存在相等数字

这时,左有序数组任何一个数大于等于右边界数。
右有续数组任何一个数小于等于右边界数。

综上,在中间数不等于numbers[j]的时候,可以以numbers[j]作为判断标准。

解答

  1. class Solution { 
  2. public int minArray(int[] numbers) { 
  3. int i = 0,j = numbers.length - 1; 
  4. while(i < j){ 
  5. int m = (i+j) / 2; 
  6. if (numbers[m] > numbers[j]) i = m + 1; 
  7. else if (numbers[m] < numbers[j]) j = m; 
  8. else j--; 
  9. } 
  10. return numbers[j]; 
  11. } 
  12. } 

复杂度分析

时间复杂度是O(logn),空间复杂度是O(1)
这是讨论区大佬的写法,好处是代码很短,其实在处理特殊情况numbers[m] == numbers[j]时,使用j--的方式,非常不直观,需要比较复杂的分类讨论来证明它的正确性。
书中的解法思路会更加直观明了,也就是当遇到有相等数字的时候,就放弃二分查找,转而逐个遍历进行查找。

解答2

  1. class Solution { 
  2. public int minArray(int[] numbers) { 
  3. int i = 0; 
  4. int j = numbers.length - 1; 
  5. while(i != j){ 
  6. int m = i + (j - i) / 2; 
  7. if(numbers[m] > numbers[j]) i = m + 1; 
  8. else if(numbers[m] < numbers[j]) j = m; 
  9. else return findMin(numbers,i,j); 
  10. } 
  11. return numbers[i]; 
  12. } 
  13.  
  14. public int findMin(int[] numbers,int i,int j){ 
  15. int result = numbers[i]; 
  16. for(int m = i;i <= j;i++){ 
  17. if (numbers[i] < result) result = numbers[i]; 
  18. } 
  19. return result; 
  20. } 
  21. } 

复杂度其实跟上面的方法没有区别,因为本质上,上面的解法在遇到相等的情况时,执行j--,其实也是一个个的去遍历。

解答3

上面的两种方法,在二分查找时,都是以numbers[j]作为比较对象,实际上也可以以numbers[i]作为比较对象。
上面分析过了,为什么二分查找时,比较的对象是numbers[j],主要就是应对pivot==0,也就是旋转位置是0,整个数组就是有序数组的情况。
所以就需要针对这种特殊情况增加一句特殊判断。即第6行的判断语句,如果判断[i,j]范围内是完全有序的数组,直接返回numbers[i]
复杂度依然不变。

  1. class Solution { 
  2. public int minArray(int[] numbers) { 
  3. int i = 0; 
  4. int j = numbers.length - 1; 
  5. while(i != j){ 
  6. if(numbers[i] < numbers[j]) return numbers[i]; 
  7. int m = i + (j - i) / 2; 
  8. if(numbers[m] > numbers[i]) i = m + 1; 
  9. else if(numbers[m] < numbers[j]) j = m; 
  10. else return findMin(numbers,i,j); 
  11. } 
  12. return numbers[i]; 
  13. } 
  14.  
  15. public int findMin(int[] numbers,int i,int j){ 
  16. int result = numbers[i]; 
  17. for(int m = i;i <= j;i++){ 
  18. if (numbers[i] < result) result = numbers[i]; 
  19. } 
  20. return result; 
  21. } 
  22. } 
posted @ 2020-06-14 19:56  Howfar's  阅读(205)  评论(0)    收藏  举报