Skip to content

Latest commit

 

History

History
208 lines (147 loc) · 8.36 KB

面试题11_旋转数组的最小数字.md

File metadata and controls

208 lines (147 loc) · 8.36 KB

面试题 11 :旋转数组的最小数字


leetcode-cn 题目地址

📗difficulty:Easy

🎯Tags:


1. 题目描述📃

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [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

2. 解题思路💡

两分查找

以下思路来源于 leetcode-cn 用户 Krehets 的题解,这里对其进行抄录,感谢其的精彩分析。

一种简单的且直观的办法是从头到尾遍历一次数组,就可以找出最小值来。但是这种情况的时间复杂度为 O(n) ,并没有利用题目给出的旋转后的数组的特性。

题目中描述到,给定一个升序的数组,然后再进行旋转操作。如果一个数组是有序的,那么查找一个元素可以采用两分搜索的办法,其复杂度为 O(log n),本题目中数组旋转后,变成了 2 个有序的数组,但是依旧保留有部分特殊性质,可以尝试利用其性质。

旋转后的数组,左半部分的任意一个数 ,都大于等于右半部分的数。而且最小的元素,就是左右两部分的分界线。

设置指针 leftright 分别指向数组的两端。mid 为两分的中点。mid 为向下取整计算得出。

有以下三种的情况:

  • numbers[mid] > numbers[right]
    • mid 在左半部分,即旋转点 p 一定在 [mid + 1,right] 中,所以执行 left = mid + 1
  • numbers[mid] < numbers[right]
    • mid 在右半部分,即旋转点 p 一定在 [left,mid] 中,所以执行 right = mid
  • numbers[mid] == numbers[right]
    • 此时无法判断 mid 在哪个部分中,例如:
      • [1,0,1,1,1] :旋转点 p = 1 ,而 mid = 2 在 右排序数组 中。 例 [1,1,1,0,1] :旋转点 p = 3 ,而 mid = 2 在 左排序数组 中。
    • 这也是本题的难点所在。解决的办法是执行 right = right - 1 ,缩小判断的范围。
      • 解析:执行 right - 1 后,旋转点 p 依旧在 [left,right] 区间内。
      • 如果 mid 在右排序数组中,numbers[mid] == numbers[right] ,因此数组 [mid,right] 区间内的所有元素都相等,而 right - 1 后,旋转点 p 依旧在[left,right]区间内。
      • 如果 mid 在左排序数组中,有 numbers[p] <= numbers[right] == numbers[mid]
        • numbers[p] < numbers[right],这说明 right 的左边还有更小的数字,执行 right - 1 还能保证旋转点 p 在 [left,right] 内。
        • numbers[p] == numbers[right] 的情况下。
          • right > pright - 1 后旋转点依旧在 [left,right] 内。例如:[1,1,1,0,1]
          • right = p 时,执行 righ - 1 后,可能丢失旋转点,即 p 不在 [left,right] 的范围内。例如: [1,1,1,2,3,1] 。此时 left = 0,right = 5,p = 5。虽然丢失了旋转点 p 的索引值,但是之后的循环都是在执行 right = mid,最终返回值为 numbers[left] ,其值和 number[p] 相等。
    • 综上所述,此方法可以保证返回值 numbers[left] 一定为旋转点的值;某些情况下会 left 的索引不是旋转点 p 的值,但是此方法是可行的。
// 代码虽然简洁,但是本题需要分情况讨论,比较难以考虑
public int minArray(int[] numbers) {
    if (numbers == null || numbers.length == 0) {
        return -1;
    }

    int left = 0;
    int right = numbers.length -1;
    int mid;
    while (left < right) {
        mid = left + (right - left) / 2;
        if (numbers[mid] > numbers[right]) {
            left = mid + 1;
        } else if (numbers[mid] < numbers[right]) {
            right = mid;
        } else {
            right--;  // 简约而不简单
        }
    }
    return numbers[left];  // 简约而不简单
}

复杂度分析

  • 时间复杂度:O(log n),特殊情况下会退化到 O(n)
  • 空间复杂度:O(1)

细说此两分查找

诚然,上面分析的两分查找,能够给出正确的答案,但是其思路解析并不够完善,下面给出更加容易思考且理解的解读。

以下分析来自 leetcode-cn 用户的题解。

思路解析

public class Solution {
    // [3, 4, 5, 1, 2]
    // [1, 2, 3, 4, 5]
    // 不能使用左边数与中间数比较,这种做法不能有效地减治

    // [1, 2, 3, 4, 5]
    // [3, 4, 5, 1, 2]
    // [2, 3, 4, 5 ,1]

    public int minArray(int[] numbers) {
        int len = numbers.length;
        if (len == 0) {
            return 0;
        }
        int left = 0;
        int right = len - 1;
        while (left < right) {
            int mid = (left + right) >>> 1;
            if (numbers[mid] > numbers[right]) {
                // [3, 4, 5, 1, 2],mid 以及 mid 的左边一定不是最小数字
                // 下一轮搜索区间是 [mid + 1, right]
                left = mid + 1;
            } else if (numbers[mid] == numbers[right]) {
                // 只能把 right 排除掉,下一轮搜索区间是 [left, right - 1]
                right = right - 1;
            } else {
                // 此时 numbers[mid] < numbers[right]
                // mid 的右边一定不是最小数字,mid 有可能是,下一轮搜索区间是 [left, mid]
                right = mid;
            }
        }

        // 最小数字一定在数组中,因此不用后处理
        return numbers[left];
    }
}

使用减治的思想来解决两分查找的问题,有神奇效果。


3. 总结🎯

题目扩展

154. 寻找旋转排序数组中的最小值 II 和本题描述等完全一致,代码可以相互通用。

153. 寻找旋转排序数组中的最小值 是一个情况稍微简单的情况,其保证输入数组中,不含有重复的元素。使用 154 题的代码也可以正常通过测试。

在这种情况下的分析如下:

  • numbers[mid] < numbers[right] 时,旋转点在左边序列,执行 right = mid
  • numbers[mid] > numbers[right] 时,旋转点在右边序列,执行 left = mid + 1

最终返回 numbers[left]

public int findMin(int[] numbers) {
    if (numbers == null || numbers.length == 0) {
        return -1;
    }

    int left = 0;
    int right = numbers.length -1;
    int mid;
    while (left < right) {
        mid = left + (right - left) / 2;
        if (numbers[mid] > numbers[right]) {
            left = mid + 1;
        } else if (numbers[mid] < numbers[right]) {
            right = mid;
        }
    }
    return numbers[left];
}

复杂度分析

  • 时间复杂度:O(log n)
  • 空间复杂度:O(1)

本题考点

  • 考察应聘者对二分查找的理解。本题交换了二分查找的条件,输入的数组不是排序的,而是排序数组的一个旋转。这要求我们对两分查找的过程有深刻的理解。
  • 考察应聘者的沟通和学习能力。面试官给出了一个新的概念:数组的旋转。需要我们在很短的时间内学习、理解这个新的概念。在面试的过程中,如果面试官给出新的概念,那么我们可以主动和面试官进行沟通,多问几个问题,把概念弄清楚。
  • 考察应聘者思维的全面性。排序数组本身是数组旋转的一个特例。另外,我们要考虑到数组中有相同数字的特例。如果不能很好地处理这些特例,就很难写出让面试官满意的完美代码。