209、长度最小的子数组

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

方法一:暴力法

  暴力法是最直观的方法。初始化子数组的最小长度为无穷大,枚举数组nums中的每个下标作为子数组的开始下标对于每个开始下标 i,需要找到大于或等于 i 的最小下标 j,使得从nums[i] 到nums[j] 的元素和大于或等于 s并更新子数组的最小长度(此时子数组的长度是 j-i+1)。一直枚举到给定数组最后一个元素的小标作为子数组的开始下标。

public int minSubArrayLen3(int s, int[] nums){
        int n = nums.length;
        //数组长度为0,则直接返回0
        if (n == 0) {
            return 0;
        }
        //初始化子数组长度为一个极大值
        int ans = Integer.MAX_VALUE;
        for (int i = 0; i < n; i++) {
            int sum = 0;
            for (int j = i; j < n; j++) {
                sum += nums[j];
                if (sum >= s) {
                	//当累加和大于 s 时更新最小子数组长度
                    ans = Math.min(ans, j - i + 1);
                    break;
                }
            }
        }
        //如果ans未改变则代表所有的累加和均小于s,则返回0
        return ans == Integer.MAX_VALUE ? 0 : ans;
    }

时间复杂度:O(n^2),其中 nn 是数组的长度。需要遍历每个下标作为子数组的开始下标,对于每个开始下标,需要遍历其后面的下标得到长度最小的子数组。
空间复杂度:O(1)


方法二:双指针 *** 最优解 ***

  定义两个指针 start 和 end 分别表示子数组的开始位置和结束位置,维护变量 sum 存储子数组中的元素和(即从 nums[start] 到 nums[end] 的元素和)

初始状态下,start 和 end 都指向下标 0,sum 的值为 0。

  每一轮迭代,将 nums[end] 加到 sum,如果 sum≥s,则更新子数组的最小长度(此时子数组的长度是 end−start+1),然后将 nums[start] 从sum 中减去并将 start 右移,直到 sum。直到 end 大于等于数组的长度时为止。

public int minSubArrayLen5(int s, int[] nums){
        int n = nums.length;
        if (n == 0) {
            return 0;
        }
        int ans = Integer.MAX_VALUE;
        int start = 0, end = 0;
        int sum = 0;
        while (end < n) {
            sum += nums[end];
            while (sum >= s) {
                ans = Math.min(ans, end - start + 1);
                sum -= nums[start];
                start++;
            }
            end++;
        }
        return ans == Integer.MAX_VALUE ? 0 : ans;
    }

时间复杂度:O(n),其中 n 是数组的长度。指针 start 和 end 最多各移动 n 次。
空间复杂度:O(1)


方法三:前缀+二分法查找

  方法一的时间复杂度是 O(n^2),因为在确定每个子数组的开始下标后,找到长度最小的子数组需要 O(n) 的时间。如果使用二分查找,则可以将时间优化到 O(logn)

  为了使用二分查找,需要额外创建一个数组 sums 用于存储数组 nums 的前缀和,其中 sums[i] 表示从 nums[0] 到 nums[i−1] 的元素和。得到前缀和之后,对于每个开始下标 i,可通过二分查找得到大于或等于 i 的最小下标 bound,使得 sums[bound]−sums[i−1]≥s,并更新子数组的最小长度(此时子数组的长度是 bound−(i−1))

  因为这道题保证了数组中每个元素都为正,所以前缀和一定是递增的,这一点保证了二分的正确性。如果题目没有说明数组中每个元素都为正,这里就不能使用二分来查找这个位置了。

public int minSubArrayLen4(int s, int[] nums){
        int n = nums.length;
        if (n == 0) {
            return 0;
        }
        int ans = Integer.MAX_VALUE;
        int[] sums = new int[n + 1];
        // 为了方便计算,令 size = n + 1
        // sums[0] = 0 意味着前 0 个元素的前缀和为 0
        // sums[1] = A[0] 前 1 个元素的前缀和为 A[0]
        // 以此类推
        for (int i = 1; i <= n; i++) {
            sums[i] = sums[i - 1] + nums[i - 1];
        }
        for (int i = 1; i <= n; i++) {
            //target加上前面的累加和,即相当于从当前i值开始求累加和看是否大于原始target值
            //依次求出每个i对应的下标开始连续子数组值累加和大于target的最小长度
            int target = s + sums[i - 1];
            int bound = Arrays.binarySearch(sums, target);
            if (bound < 0) {
                bound = -bound - 1;
            }
            if (bound <= n) {
                ans = Math.min(ans, bound - (i - 1));
            }
        }
        return ans == Integer.MAX_VALUE ? 0 : ans;
    }

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

参考:
Leetcode官方题解

你可能感兴趣的:(Leetcode,指针,数据结构,leetcode)