Leetcode——长度最小的子数组 / 最短无序连续子数组 / 和为k的连续子数组

Posted Yawn,

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Leetcode——长度最小的子数组 / 最短无序连续子数组 / 和为k的连续子数组相关的知识,希望对你有一定的参考价值。

1. 长度最小的子数组

(1)暴力

使用两个 for 循环,一个 for 循环固定一个数字比如 m,另一个 for 循环从 m 的下一个元素开始累加,当和大于等于 s 的时候终止内层循环,顺便记录下最小长度

class Solution {
    public int minSubArrayLen(int s, int[] nums) {
        int min = Integer.MAX_VALUE;
        for (int i = 0; i < nums.length; i++) {
            int sum = nums[i];
            //如果一个数字就大于s,直接返回即可
            if (sum >= s)
                return 1;
            for (int j = i + 1; j < nums.length; j++) {
                sum += nums[j];
                if (sum >= s) {
                    min = Math.min(min, j - i + 1);
                    break;
                }
            }
        }

        if (min == Integer.MAX_VALUE)
            return 0;
        else
            return min;
    }
}

(2)滑动窗口

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int left = 0;
        int min = nums.length + 1;  //为了判断不存在数组和 >= target的情况
        int sum = 0;

        for (int right = 0; right < nums.length; right++) {
            sum = sum + nums[right];
            while (sum >= target) {
                if (right - left + 1 < min) {
                    min = right - left + 1;
                }

                sum = sum - nums[left];
                left++;
            }
        }
        
        //min大于数组长度,返回0
        if (min > nums.length)
            return 0;
        else
            return min;
    }
}

2. 最短无序连续子数组

(1)双指针 + 排序

最终目的是让整个数组有序,那么我们可以先将数组拷贝一份进行排序,然后使用两个指针 i 和 j 分别找到左右两端第一个不同的地方,那么 [i, j] 这一区间即是答案。

  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(n)
class Solution {
    public int findUnsortedSubarray(int[] nums) {
        int n = nums.length;
        int[] arr = nums.clone();
        Arrays.sort(arr);
        int i = 0, j = n - 1;
        while (i <= j && nums[i] == arr[i]) 
        	i++;
        while (i <= j && nums[j] == arr[j]) 
        	j--;
        return j - i + 1;
    }
}

(2)双指针 + 线性扫描

另外一个做法是,我们把整个数组分成三段处理。

  • 时间 O(n) ,空间 O(1)
  • 起始时,先通过双指针 i 和 j 找到左右两次侧满足 单调递增 的分割点。
  • 即此时 [0, i] 和 [j, n)满足升序要求,而中间部分 (i, j) 不确保有序。
  • 然后我们对中间部分 [i, j]进行遍历:
    • 发现 nums[x] < nums[i - 1]:由于对 [i, j] 部分进行排序后 nums[x] 会出现在 nums[i - 1]后,将不满足整体升序,此时我们需要调整分割点 i 的位置;
    • 发现 nums[x] > nums[j + 1]:由于对 [i, j]部分进行排序后 nums[x] 会出现在 nums[j + 1] 前,将不满足整体升序,此时我们需要调整分割点 j 的位置。

在调整 i 和 j 的时候,我们可能会到达数组边缘,这时候可以建立两个哨兵:数组左边存在一个足够小的数,数组右边存在一个足够大的数。

思路过程:

通过双指针 i 和 j 找到左右两侧满足 单调递增 的分割点。
比如 [2,6,4,8,10,9,15] 找到的 i => 1, j => 5; nums[1] = 6, nums[5] = 9
对找到的分割点 i, j ; [0,i][j,n) 都是单调递增的,中间部分 (i + 1,j - 1) 不确保有序。

记录下初始的分割点 l = i, r = j,  对中间部分 [l,r] 遍历调整分割点,
[2,6,4,8,10,9,15]6,4,8,10,9】
	l = i = 1 对应 6	
	r = j = 5 对应9

对 nums[k](l <= k <= r) :
	若 nums[k] < nums[i], i 向左调整一位,  
	若 nums[k] > nums[j], j 向右调整一位  

遍历完中间部分,左右分割点 i ,j 之前的子数组 [i + 1, j - 1] 就是最终求的最短无序连续子数组

实现代码:

class Solution {
    int MIN = -100005, MAX = 100005;
    public int findUnsortedSubarray(int[] nums) {
        int n = nums.length;
        int i = 0, j = n - 1;
        
		// 通过双指针 i 和 j 找到左右两侧满足 单调递增 的分割点。
        // [0,i] 和 [j,n) 都是单调递增的,中间部分 (i,j) 不确保有序。
        while (i < j && nums[i] <= nums[i + 1]) i++;
        while (i < j && nums[j] >= nums[j - 1]) j--;
		
		//nums[x] < nums[i - 1]:由于对 [i + 1, j - 1] 部分进行排序后 nums[x] 会出现在 nums[i - 1]后,将不满足整体升序,此时我们需要调整分割点 i 的位置;
		//[2,6,4,8,10,9,15]  【6,4,8,10,9】 也就是 4 < 6
        int l = i, r = j;
        int min = nums[i], max = nums[j];
        for (int u = l; u <= r; u++) {
            if (nums[u] < min) {		
                while (i >= 0 && nums[i] > nums[u]) //调整分割点 i
                	i--;
                min = i >= 0 ? nums[i] : MIN;
            }
            if (nums[u] > max) {
                while (j < n && nums[j] < nums[u]) // 调整分割点 j
                	j++;
                max = j < n ? nums[j] : MAX;
            }
        }
        // [i + 1, j - 1] 就是最短无序连续子数组
        return j == i ? 0 : (j - 1) - (i + 1) + 1;
    }
}

(3)单调栈

  • 维护一个单调递减栈,保证栈顶下标元素一定是当前栈中最大的。
  • 从左往右扫一遍找到左边界,再从右往左扫一遍找到右边界,两者相减即可。
class Solution {
    public int findUnsortedSubarray(int[] nums) {
        // 单调栈从前往后遍历一遍可得到左边界
        // 单调栈从后往前遍历一遍可得到右边界
        Deque<Integer> stack = new ArrayDeque<>();
        int left = nums.length;
        for (int i = 0; i < nums.length; i++) {
            while (!stack.isEmpty() && nums[stack.peek()] > nums[i]) {
                left = Math.min(left, stack.pop());
            }
            stack.push(i);
        }

        stack.clear();
        int right = -1;
        for (int i = nums.length - 1; i >= 0; i--) {
            while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) {
                right = Math.max(right, stack.pop());
            }
            stack.push(i);
        }

        return right - left > 0 ? right - left + 1 : 0;
    }
}

3. 和为k的连续子数组

(1)暴力

考虑:

  • 元素是否存在负数
  • 需要连续子数组
class Solution {
    public int subarraySum(int[] nums, int k) {
        int len = nums.length;
        int count = 0;
        for (int left = 0; left < len; left++) {
            for (int right = left; right < len; right++) {
                int sum = 0;
                for (int i = left; i <= right; i++) {
                    sum += nums[i];
                }
                if (sum == k) {
                    count++;
                }
            }
        }
        return count;
    }

}

(2)暴力(优化)

  • 固定了起点,即先固定左边界,然后枚举右边界哈,时间复杂度降了一维。
class Solution {
    public int subarraySum(int[] nums, int k) {
        int count = 0;
        int len = nums.length;
        for (int left = 0; left < len; left++) {
            int sum = 0;
            // 区间里可能会有一些互相抵销的元素
            for (int right = left; right < len; right++) {
                sum += nums[right];
                if (sum == k) {
                    count++;
                }
            }
        }
        return count;
    }
}

或者:

class Solution {
    public int subarraySum(int[] nums, int k) {
         int len = nums.length;
         int sum = 0;
         int count = 0;
         //双重循环
         for (int i = 0; i < len; ++i) {
             for (int j = i; j < len; ++j) {
                 sum += nums[j];
                 //发现符合条件的区间
                 if (sum == k) {
                     count++;
                 }
             }
             //记得归零,重新遍历
             sum = 0;
         }
         return count;
    }
}

(3)前缀和

  • 前缀和其实我们很早之前就了解过的,我们求数列的和时,Sn = a1+a2+a3+…an; 此时Sn就是数列的前 n 项和。

  • 例 S5 = a1 + a2 + a3 + a4 + a5; S2 = a1 + a2。所以我们完全可以通过 S5-S2 得到 a3+a4+a5 的值,这个过程就和我们做题用到的前缀和思想类似。我们的前缀和数组里保存的就是前 n 项的和。

  • 我们通过前缀和数组保存前 n 位的和,presum[1]保存的就是 nums 数组中前 1 位的和,也就是 presum[1] = nums[0], presum[2] = nums[0] + nums[1] = presum[1] + nums[1]. 依次类推,所以我们通过前缀和数组可以轻松得到每个区间的和。

  • 例如我们需要获取 nums[2] 到 nums[4] 这个区间的和,我们则完全根据 presum 数组得到,是不是有点和我们之前说的字符串匹配算法中 BM,KMP 中的 next 数组和 suffix 数组作用类似。那么我们怎么根据 presum 数组获取 nums[2] 到 nums[4] 区间的和呢?见下图

    我们可以通过下面这段代码得到我们的前缀和数组,非常简单:

 for (int i = 0; i < nums.length; i++) {
      presum[i+1] = nums[i] + presum[i];
 }

我们看看直接使用前缀和怎么写

class Solution {
    public int subarraySum(int[] nums, int k) {
        //前缀和数组
        int[] presum = new int[nums.length+1];
        for (int i = 0; i < nums.length; i++) {
            //这里需要注意,我们的前缀和是presum[1]开始填充的
            presum[i+1] = nums[i] + presum[i];
        }
        //统计个数
        int count = 0;
        for (int i = 0; i < nums.length; ++i) {
            for (int j = i; j < nums.length; ++j) {
                //注意偏移,因为我们的nums[2]到nums[4]等于presum[5]-presum[2]
                //所以这样就可以得到nums[i,j]区间内的和
                if (presum[j+1] - presum[i] == k) {
                    count++;
                }
            }
        }
        return count;
    }
}

直接使用前缀和,发现该代码虽然用到了前缀和数组,但是对比暴力法并没有提升性能,时间复杂度仍为O(n^2),空间复杂度成了 O(n)。那我们有没有其他方法解决呢?

(4)前缀和 + 哈希表优化

由于只关心次数,不关心具体的解,我们可以使用哈希表加速运算;

由于保存了之前相同前缀和的个数,计算区间总数的时候不是一个一个地加,时间复杂度降到了 O(N)

解释一开始 preSumFreq.put(0, 1); 的意义

  • 计算完包括了当前数前缀和以后,我们去查一查在当前数之前,有多少个前缀和等于 preSum - k 呢?
    这是因为满足 preSum - (preSum - k) == k 的区间的个数是我们所关心的。

  • 对于一开始的情况,下标 0 之前没有元素,可以认为前缀和为 0,个数为 1 个,
    因此 preSumFreq.put(0, 1);,这一点是必要且合理的。

class Solution {
    public int subarraySum(int[] nums, int k) {
        // key:前缀和,value:key 对应的前缀和的个数
        Map<Integer, Integer> preSumFreq = new HashMap<>();
        // 对于下标为 0 的元素,前缀和为 0,个数为 1
        preSumFreq.put(0, 1);

        int preSum = 0;
        int count = 0;
        for (int num : nums) {
            preSum += num;

            // 先获得前缀和为 preSum - k 的个数,加到计数变量里
            if (preSumFreq.containsKey(preSum - k)) {
                count += preSumFreq.get(preSum - k);
            }

            // 然后维护 preSumFreq 的定义
            preSumFreq.put(preSum, preSumFreq.getOrDefault(preSum, 0) + 1);
        }
        
        return count;
    }
}

以上是关于Leetcode——长度最小的子数组 / 最短无序连续子数组 / 和为k的连续子数组的主要内容,如果未能解决你的问题,请参考以下文章

leetcode581 最短无序连续子数组(Easy不简单)

LeetCode 581. 最短无序连续子数组(Shortest Unsorted Continuous Subarray)

581-最短无序连续子数组

最短无序连续子数组

LeetCode 581 最短无序连续子数组[排序] HERODING的LeetCode之路

力扣----16. 最短无序连续子数组(JavaScript实现)