• 剑指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. } 
  • 相关阅读:
    HDU
    POJ
    快速幂运算
    RocketMQ集群
    RocketMQ角色介绍
    RocketMQ初探
    MySQL 串行隔离级别小记
    分布式事务笔记
    MySQL分库分表篇
    MySQL主从篇
  • 原文地址:https://www.cnblogs.com/Howfars/p/13126645.html
Copyright © 2020-2023  润新知