LeetCode 153、154 寻找排序数组中的最小值

197 阅读1分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

题目:已知一个长度为n的升序数组,将此数组n次旋转后可得到一个输入数组,要求设计一个时间复杂度为O(logn)O(logn)的算法返回输入数组中的最小值。

关于旋转数组:数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。

LeetCode 153中,原数组中的所有元素互不相同。而在LeetCode 154中,原数组中可能存在相同的元素,同时也对应剑指Offer的第11题

解题思路

这三题的万能解法,可以对数组进行排序,返回第一个元素即可,代码如下:

public int findMin(int[] nums) {
	Arrays.sort(nums);
    return nums[0];
}

时间复杂度为O(nlogn)O(nlogn), 显然不符合要求,并且不讲武德!

另一种方法,直接遍历整个数组,记录一下最小值不就行了~代码如下:

public int findMin(int[] nums) {
    int min = nums[0];
    for(int i=1;i<nums.length;i++){
        min = Math.min(min, nums[i]);
    }
    return min;
}

时间复杂度为O(n)O(n),题目是做出来了,但出题人的良苦用心就没用上~

既然题目要求时间复杂度为O(logn)O(logn),那么首先想到的就是二分法,并且注意,输入数组是原始的有序数组进行旋转后得到的,也就是输入数组在局部还是有序的,这就让我们使用二分存在了可能性,最基本的想法是将数组分成三部分:

​ 左边 || 中间 || 右边

其中中间就代表这个数组的中间元素,我们只需要找到左边的最小值和右边的最小值之后和中间的比较即可得到最终答案,答案就是递归,代码如下:

public int findMin(int[] nums) {
    return findByMid(nums, 0, nums.length-1);
}

public int findByMid(int[] nums, int left, int right){
    if(left==right) return nums[left];
    int mid = left + (right-left) / 2;
    int curMin = nums[mid];
    if(left<=right&&mid-1>=0) {
        int leftMin = findByMid(nums, left, mid - 1);
        curMin = Math.min(curMin, leftMin);
    }
    if(left<=right&&mid+1<nums.length) {
        int rightMin = findByMid(nums, mid + 1, right);
        curMin = Math.min(curMin, rightMin);
    }

    return curMin;
}

题目是做出来了,但又显然做复杂了。。。并且需要注意,上述代码虽然使用了二分的思想,但时间复杂度还是O(n)O(n),并且空间复杂度也是O(n)O(n)

上面三种方法对上面的三题都可以AC,现在思考时间复杂度如何才能为O(logn)O(logn),我们可以想象,对于旋转后的输入数组:

  • 如果数组的中间元素大于数组的最右边元素,那么最小值一定在数组的右边。
  • 如果数组的中间元素小于数组的最右边元素,那么最小值一定在数组的左边。

那么对于数组中无重复元素的153题就可以做了,我们只需要不断更新左右边界即可得到最终解,也即最普通的二分查找,代码如下:

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

但如果存在数组中元素相同,例如旋转数组:[3, 3, 3, 1, 2, 3]。上面的代码则无法AC,此时我们无法判断最小值在数组的左边还是右边,因此我们只能不断缩小右边的范围,直到找到不相等的,再次判断即可,代码如下:

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

最终时间复杂度为O(logn)O(logn)