LeetCode581.最短无序连续子数组Java题解

问题复现

给定一个整数数组,你需要寻找一个连续的子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。

你找到的子数组应是最短的,请输出它的长度。

示例 1:

输入: [2, 6, 4, 8, 10, 9, 15]
输出: 5
解释: 你只需要对 [6, 4, 8, 10, 9] 进行升序排序,那么整个表都会变为升序排序。
说明 :
输入的数组长度范围在 [1, 10,000]。
输入的数组可能包含重复元素 ,所以升序的意思是<=。

方法 1:暴力

算法

在暴力方法中,我们考虑 numsnums 数组中每一个可能的子序列。对于每一个子序列 nums[i:j]nums[i:j] ,我们检查它是否是最小的无序子序列。因此对于每一个子序列,我们求出这个子序列中的最大和最小值,并分别用 maxmax 和 minmin 表示。

如果子序列 nums[0:i-1]nums[0:i−1] 和 nums[j:n-1]nums[j:n−1] 是升序的,那么仅有 nums[i:j]nums[i:j] 是可能的子序列。更进一步, nums[0:i-1]nums[0:i−1] 中所有的元素都要比 minmin 小且 nums[j:n-1]nums[j:n−1] 中所有的元素都要比 maxmax 大。我们对于枚举的每一对 ii 和 jj 都做这样的检查。

接下来,我们需要检查 nums[0:i-1]nums[0:i−1] 和 nums[j:n-1]nums[j:n−1] 是否是升序的。如果上述所有条件都满足,我们通过枚举所有的 i 和 j 并计算 j-ij−i 来找到最短的无序子数组。

public class Solution {
    public int findUnsortedSubarray(int[] nums) {
        int res = nums.length;
        for (int i = 0; i < nums.length; i++) {
            for (int j = i; j <= nums.length; j++) {
                int min = Integer.MAX_VALUE, max = Integer.MIN_VALUE, prev = Integer.MIN_VALUE;
                for (int k = i; k < j; k++) {
                    min = Math.min(min, nums[k]);
                    max = Math.max(max, nums[k]);
                }
                if ((i > 0 && nums[i - 1] > min) || (j < nums.length && nums[j] < max))
                    continue;
                int k = 0;
                while (k < i && prev <= nums[k]) {
                    prev = nums[k];
                    k++;
                }
                if (k != i)
                    continue;
                k = j;
                while (k < nums.length && prev <= nums[k]) {
                    prev = nums[k];
                    k++;
                }
                if (k == nums.length) {
                    res = Math.min(res, j - i);

                }
            }
        }
        return res;
    }
}

复杂度分析

时间复杂度:O(n^3) 使用了三重循环。

空间复杂度:O(1)只使用了常数空间。

方法 2:更好的暴力

算法

在这种方法中,我们基于选择排序使用如下想法:我们遍历 nums 数组中的每一个元素 nums[i] 。对于每一个元素,我们尝试找到它在正确顺序数组中的位置,即将它与每一个满足 i < j < n的 nums[j]做比较,这里 n 是 nums 数组的长度。

如果存在 nums[j] 比 nums[i]小,这意味着 nums[i]和 nums[j] 都不在排序后数组中的正确位置。因此我们需要交换这两个元素使它们到正确的位置上。但这里我们并不需要真的交换两个元素,我们只需要标记两个元素在原数组中的位置 i 和 j。这两个元素标记着目前无序数组的边界。

因此,在所有的 nums[i] 中,我们找到最左边不在正确位置的 nums[i],这标记了最短无序子数组的左边界)。类似的,我们找到最右边不在正确位置的边界 nums[j] ,它标记了最短无序子数组的右边界 。
图1

因此,我们可以求得最短无序子数组的长度为 r - l + 1 。


public class Solution {
    public int findUnsortedSubarray(int[] nums) {
        int l = nums.length, r = 0;
        for (int i = 0; i < nums.length - 1; i++) {
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[j] < nums[i]) {
                    r = Math.max(r, j);
                    l = Math.min(l, i);
                }
            }
        }
        return r - l < 0 ? 0 : r - l + 1;
    }
}

复杂度分析

时间复杂度:O(n^2)使用了两重循环。

空间复杂度:O(1)只使用了常数空间。

方法 3:排序

算法

另一个简单的想法是:我们将数组 nums 进行排序,记为 nums_sorted 。然后我们比较 nums 和 nums_sorted 的元素来决定最左边和最右边不匹配的元素。它们之间的子数组就是要求的最短无序子数组。


public class Solution {
    public int findUnsortedSubarray(int[] nums) {
        int[] snums = nums.clone();
        Arrays.sort(snums);
        int start = snums.length, end = 0;
        for (int i = 0; i < snums.length; i++) {
            if (snums[i] != nums[i]) {
                start = Math.min(start, i);
                end = Math.max(end, i);
            }
        }
        return (end - start >= 0 ? end - start + 1 : 0);
    }
}

复杂度分析

时间复杂度:O(nlogn) 。排序消耗nlogn 的时间。

空间复杂度:O(n)。我们拷贝了一份原数组来进行排序。

方法 4:使用栈

算法

这个方法背后的想法仍然是选择排序。我们需要找到无序子数组中最小元素和最大元素分别对应的正确位置,来求得我们想要的无序子数组的边界。

为了达到这一目的,此方法中,我们使用 栈栈 。我们从头遍历 nums数组,如果遇到的数字大小一直是升序的,我们就不断把对应的下标压入栈中,这么做的目的是因为这些元素在目前都是处于正确的位置上。一旦我们遇到前面的数比后面的数大,也就是 nums[j]比栈顶元素小,我们可以知道 nums[j] 一定不在正确的位置上。

为了找到 nums[j] 的正确位置,我们不断将栈顶元素弹出,直到栈顶元素比 nums[j]小,我们假设栈顶元素对应的下标为 k ,那么我们知道 nums[j] 的正确位置下标应该是 k + 1。

我们重复这一过程并遍历完整个数组,这样我们可以找到最小的 k, 它也是无序子数组的左边界。

类似的,我们逆序遍历一遍 nums 数组来找到无序子数组的右边界。这一次我们将降序的元素压入栈中,如果遇到一个升序的元素,我们像上面所述的方法一样不断将栈顶元素弹出,直到找到一个更大的元素,以此找到无序子数组的右边界。

我们可以看下图作为参考。我们观察到上升还是下降决定了相对顺序,我们还可以观察到指针 b在下标 0 后面标记着无序子数组的左边界,指针 a在下标 7 前面标记着无序子数组的右边界。


public class Solution {
    public int findUnsortedSubarray(int[] nums) {
        Stack < Integer > stack = new Stack < Integer > ();
        int l = nums.length, r = 0;
        for (int i = 0; i < nums.length; i++) {
            while (!stack.isEmpty() && nums[stack.peek()] > nums[i])
                l = Math.min(l, stack.pop());
            stack.push(i);
        }
        stack.clear();
        for (int i = nums.length - 1; i >= 0; i--) {
            while (!stack.isEmpty() && nums[stack.peek()] < nums[i])
                r = Math.max(r, stack.pop());
            stack.push(i);
        }
        return r - l > 0 ? r - l + 1 : 0;
    }
}

复杂度分析

时间复杂度:O(n)。需要遍历数组一遍,栈的时间复杂度也为 O(n)。

空间复杂度:O(n)) 。

方法 5:不使用额外空间

算法

这个算法背后的思想是无序子数组中最小元素的正确位置可以决定左边界,最大元素的正确位置可以决定右边界。

因此,首先我们需要找到原数组在哪个位置开始不是升序的。我们从头开始遍历数组,一旦遇到降序的元素,我们记录最小元素为 min 。

类似的,我们逆序扫描数组 nums,当数组出现升序的时候,我们记录最大元素为 max。

然后,我们再次遍历 nums 数组并通过与其他元素进行比较,来找到 min和 max在原数组中的正确位置。我们只需要从头开始找到第一个大于 min的元素,从尾开始找到第一个小于 max 的元素,它们之间就是最短无序子数组。

我们可以再次使用下图作为说明:

我们观察到指针 b在下标 0 以后,标记着无序子数组的左边界,指针 a在下标 7 以前,标记着无序子数组的右边界。


public class Solution {
    public int findUnsortedSubarray(int[] nums) {
        int min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
        boolean flag = false;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] < nums[i - 1])
                flag = true;
            if (flag)
                min = Math.min(min, nums[i]);
        }
        flag = false;
        for (int i = nums.length - 2; i >= 0; i--) {
            if (nums[i] > nums[i + 1])
                flag = true;
            if (flag)
                max = Math.max(max, nums[i]);
        }
        int l, r;
        for (l = 0; l < nums.length; l++) {
            if (min < nums[l])
                break;
        }
        for (r = nums.length - 1; r >= 0; r--) {
            if (max > nums[r])
                break;
        }
        return r - l < 0 ? 0 : r - l + 1;
    }
}

复杂度分析

时间复杂度:O(n)。使用了 4 个 O(n)的循环。

空间复杂度:O(1)。使用了常数空间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值