【LeetCode闲暇一题】209. 长度最小的子数组【中等】

题目:

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s
的长度最小的连续子数组,并返回其长度。如果不存在符合条件的连续子数组,返回 0。

示例:
输入:s = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的连续子数组。

进阶:
如果你已经完成了 O(n) 时间复杂度的解法, 请尝试 O(n log n) 时间复杂度的解法。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-size-subarray-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

我的思路:

使用窗口法。一个窗口表示一个子数组。首先从0进行遍历加值以初始化窗口大小以及满足条件时的加和。然后以此窗口为基础。滑动之前先减去窗口的第一个元素看加和是否依然符合条件,如果符合条件说明窗口可以缩小,就将窗口缩小一格.当窗口不能缩小时就将窗口往后滑动一格.然后再次减去第一个元素进行检查。一直循环到窗口无法缩小,且窗口无法在往后滑动,此时窗口的大小即为长度最小的连续数组的长度。

我的代码:

public int minSubArrayLen(int s, int[] nums) {
     
    int index = 0;
    int resultNum = 0;
    int addNum = 0;
    // 初始化加和addNum
    for (index = 0; index < nums.length;index++) {
     
        addNum += nums[index];
        if(addNum >= s){
     
            break;
        }
    }
    // 如果addNum>=s,即数组中存在相应的子数组使得addNum>=s
    if (addNum >= s) {
     
        // 初始化resultNum为最后的一个下标+1(因为下标是从0开始的)
        resultNum = index+1;
        // 这里的index指下一个将要加的数值的下标
        index = index+1;
        // 一直循环下去,直到窗口无法再往后滑动及调整(数组内所有数值被加完且,addNum
        while(true) {
     
            /* addNum减去产生这个addNum的子数组中的第一个数,即下标为index-resultNum的数.
               一是为了看减去第一个元素后addNum是否还大于等于s,如果是的话可以调整窗口大小(result-1),然后继续循环减去首个元素检查是否符合条件
               二是如果减去首个元素后不符合条件,那么接下来就需要往后滑动一格窗口(窗口是只能缩小不能扩大的,新纳入一个元素必然要去掉一个元素)。
               窗口往后滑动时无需重新对整个窗口元素进行加和,仅需用减去首个元素的addNum+nums[index]即可得到窗口滑动后的元素加和
            */
            addNum = addNum-nums[index-resultNum];
            // 减去首个元素addNum还大于等于s,说明调整后的窗口内的元素也符合条件,因为减去了首个元素,所以resultNum-1
            // 此时index无需进行+1滑动窗口.只需继续循环检查
            if(addNum >= s) {
     
                resultNum--;
            } else {
     
                // 如果窗口的值已经不满足条件,那么就要滑动窗口
                // 如果窗口已经滑到头了,后面没有元素了,那说明数组已遍历完,可以跳出
                if (index == nums.length) {
     
                    break;
                }
                // 得出滑动后的窗口元素加和
                addNum = addNum+nums[index];
                // 无论这个窗口加和addNum满不满足条件index都要+1.因为已经把这个元素纳入了addNum了
                index++;
            }
        }
    }
    // 返回窗口的大小
    return resultNum;
}

运行结果:
在这里插入图片描述

官方题解下的其它思路:

暴力法:

直接进行遍历,遍历以每个数组元素开始,满足条件时的子数组长度,然后记录最小的长度。
时间复杂度: O ( n 2 ) O(n^2) O(n2),其中 n 是数组的长度。需要遍历每个下标作为子数组的开始下标,对于每个开始下标,需要遍历其后面的下标得到长度最小的子数组。
空间复杂度: O ( 1 ) O(1) O(1)
解法代码:

public static int minSubArrayLen2(int s, int[] nums) {
     
    int minSubLen = Integer.MAX_VALUE;
    for (int i = 0; i < nums.length; i++) {
     
        int addNum = 0;
        for (int j = 0; j < nums.length-i; j++) {
     
            addNum += nums[i+j];
            if (addNum>=s) {
     
                minSubLen = Math.min(minSubLen, j+1);
                break;
            }
        }
    }
    return minSubLen == Integer.MAX_VALUE ? 0 : minSubLen;
}

运行结果:
在这里插入图片描述

前缀和+二分查找法:

前缀和即用一个长度为原数组长度+1的数组snums,在该数组每一个下标i处存储原数组[0,i-1]所有元素的加和。因为题目保证原数组的所有元素都是正数,因此前缀和数组一定是递增的。那么前缀和数组两个元素之前的差值即为该区间内原数组子数组的加和值。只需找到第一个大于等于snums[i]+s的数组下标j,再用j-i即可得到符合条件的子数组长度。而这个查找操作可以使用二分查找法来实现。

时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn),其中 nn 是数组的长度。需要遍历每个下标作为子数组的开始下标,遍历的时间复杂度是 O ( n ) O(n) O(n),对于每个开始下标,需要通过二分查找得到长度最小的子数组,二分查找得时间复杂度是 O ( log ⁡ n ) O(\log n) O(logn),因此总时间复杂度是 O ( n log ⁡ n ) O(n \log n) O(nlogn)
空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组的长度。额外创建数组 s u m s sums sums 存储前缀和。

实现代码:

public static int minSubArrayLen3(int s, int[] nums) {
     
        int[] snums = new int[nums.length+1]; 
        snums[0] = 0;
        int minSubLen = Integer.MAX_VALUE;
        for (int i = 0; i < nums.length; i++) {
     
            snums[i+1] = snums[i] + nums[i];
        }
        for (int i = 0; i < snums.length; i++) {
     
            // 寻找前缀和大于等于snums[i] + s的
            int sum = snums[i] + s;
            // 如果存在相等的bound-i就是长度
            int bound = Arrays.binarySearch(snums, sum);
            // 如果前缀和存在相等的,则返回相等时的下标:
            // 如果前缀和存在不相等的,有三种情况:
            // 1.前缀和数组中的元素均小于sum,那么就会返回-(snums.length+1),这时表示以i为起点的子数组中不符合满足条件的
            // 2.前缀和数组中的元素均大于sum,那么就会返回-1,这种情况不会存在.因为元素均为正整数.导致sums是递增的,snums[i]再加一个正整数,会导致sunms必然存在小于sum的
            // 2.前缀和数组中的元素均既有大于sum,又有小于sum,但是没有等于sum,那么就会返回-(bound+1),bound标识第一个大于sum的元素
            if (bound < 0) {
     
                bound = -bound-1;
            }
            // 如果这个bound大于nums.length,说明不包含符合条件的子数组,无需计算(bound是sunms的下标,sunms的长度是nums.length+1,那么bound的最大有效值就是nums.length了)
            if (bound<=nums.length) {
     
                // bound-i获得长度
                minSubLen = Math.min(minSubLen, bound-i);
            }
        }
        return minSubLen == Integer.MAX_VALUE ? 0 : minSubLen;
    }

运行结果:
在这里插入图片描述

双指针法:

双指针法和窗口法的思路相似,用两个变量start和end标记当前子数组的开头和结束在母数组中的下标,子数组长度为end-start+1,当子数组加和大于等于s时,就计算子数组长度并比较记录,然后start+1,移去子数组的首个元素,当子数组加和小于s时,就end+1纳入下一个元素。知道end无法再加则结束循环.

时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组的长度。指针 s t a r t start start e n d end end 最多各移动 n n n 次。
空间复杂度: O ( 1 ) O(1) O(1)
实现代码:

public static int minSubArrayLen4(int s, int[] nums) {
     
        int minSubLen = Integer.MAX_VALUE;
        int addNum = 0;
        int start = 0;
        int end = 0;
        // 如果end还在范围内
        while(end < nums.length) {
     
            // 加上end获得当前子数组的加和
            addNum = addNum+nums[end];
            // 如果加和>=s就一直削减子数组长度
            while (addNum >= s) {
     
                // 当前的子数组长度
                minSubLen = Math.min(minSubLen, end-start+1);
                // 削减掉子数组的首个元素
                addNum -= nums[start];
                start++;
            }
            // 往后递加
            end++;
        }
        return minSubLen == Integer.MAX_VALUE ? 0 : minSubLen;
    }

运行结果:
在这里插入图片描述

参考资料:

209. 长度最小的子数组—力扣官方题解
Java之数组查询Arrays类的binarySearch()方法详解

你可能感兴趣的:(算法,LeetCode,JAVA,算法,java,leetcode)