开源地址

为了便于大家学习,所有实现均已开源。欢迎 fork + star~

https://github.com/houbb/leetcode

题目

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其总和大于等于 target 的长度最小的 子数组

[numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

示例 1:

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

输入:target = 4, nums = [1,4,4] 输出:1 示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1] 输出:0

提示:

1 <= target <= 10^9 1 <= nums.length <= 10^5 1 <= nums[i] <= 10^4

v1-滑动窗口

思路

我们从第一个元素开始,step 从 1 … n。开始的下标也从 0…n。

当然这个性能估计很差。

代码

/**
     * 使用 slide window 实现
     *
     * 1. step 从 1 到 len
     *
     * @param target
     * @param nums
     * @return
     */
    public int minSubArrayLen(int target, int[] nums) {
        for(int step = 0; step < nums.length; step++) {
            for(int i = 0; i < nums.length - step; i++) {
                if(fitSum(nums, i, i+step, target)) {
                    return step+1;
                }
            }
        }

        return 0;
    }

    private boolean fitSum(int[] nums,
                        int startIndex,
                        int endIndex,
                           int target) {
        int sum = 0;
        for(int i = startIndex; i <= endIndex; i++) {
            sum += nums[i];

            if(sum >= target) {
                return true;
            }
        }

        return false;
    }

复杂度为 O(n^3)

效果

18 / 21 个通过的测试用例

超时

V2-优化

优化思路

为什么我们会超时呢?

因为每一次都要重复计算数组的和。

那么,有没有一种方法,可以让我们非常快捷的计算任意两个下标之间的差呢?

前缀和

prefix_sum[i] 表示数组 arrarr[0]arr[i] 的累加和。

具体来说:

prefix_sum[0] = arr[0]
prefix_sum[1] = arr[0] + arr[1]
prefix_sum[2] = arr[0] + arr[1] + arr[2]
...
prefix_sum[i] = arr[0] + arr[1] + ... + arr[i]

我们就得到了地推公式:

prefix_sum[i] = prefix_sum[i-1] + ... + arr[i]

整体思路

我们可以首先 one-pass 用 O(n) 把前缀和数组构建好,然后再用上面的双指针试试?

代码

我们

public int minSubArrayLen(int target, int[] nums) {
        // 构建前缀和数组
        int[] prefixSum = new int[nums.length];
        prefixSum[0] = nums[0];
        for(int i = 1; i < nums.length; i++) {
            prefixSum[i] = prefixSum[i-1] + nums[i];
        }

        for(int step = 0; step < nums.length; step++) {
            for(int i = 0; i < nums.length - step; i++) {
                int sum = prefixSum[i+step] - prefixSum[i] + nums[i];
                if(sum >= target) {
                    return step+1;
                }
            }
        }

        return 0;
    }

复杂度为 O(n^2)

效果

18 / 21 个通过的测试用例

超时

有点离谱,感觉这个 18 直接对优化卡的太死了。

v3-如何进一步优化呢?

二分法

我们慢还有一个原因就是在找下个窗口的时候,是傻傻的从零开始找的。

但是实际上应该怎么找呢?

因为是含有 n 个正整数的数组

所以 prefixSum 一定是递增的。

我们在找到 i 开始的位置后,直接用二分法寻找剩余的值位置在哪里,这样可以把复杂度降低到 O(n) * O(logn)

代码

   // 判断长度为mid的子数组和是否能够大于等于target
    private boolean isOver(int[] sum, int mid, int target) {
        int n = sum.length;
        for (int i = mid; i < n; i++) {
            if ((sum[i] - sum[i - mid]) >= target) {
                return true;
            }
        }
        return false;
    }

    public int minSubArrayLen(int target, int[] nums) {
        int n = nums.length;
        int[] sum = new int[n + 1];  // 前缀和数组
        for (int i = 1; i <= n; i++) {
            sum[i] = sum[i - 1] + nums[i - 1];  // 计算前缀和
        }

        int left = 1, right = n;
        while (left <= right) {  // 二分查找
            int mid = (right - left) / 2 + left;
            // 没找过,去右边找
            if (!isOver(sum, mid, target)) {
                left = mid + 1;
            } else {
                // 超过了 去左边找
                right = mid - 1;
            }
        }

        return left == n + 1 ? 0 : left;  // 如果没有找到满足条件的子数组,返回0
    }

效果

3ms,击败 6.18%

小结

这种如果从前缀和的思路一直考虑下来,还算是比较好想到的一个解法。

不过手写二分确实需要多习惯一下,太久不写又忘记了。

v4-双指针解法

思路

我们通过双指针。

sum 表示 left…right 的总和

首先扩大窗口,当 sum >= target 的时候,可以尝试缩小窗口。保持 right 位置不变,只是 left 向右移动。

这样满足的条件的就是最小的 len。

核心代码

    /**
     * 使用 slide window 实现
     *
     * 1. step 从 1 到 len
     *
     * @param target
     * @param nums
     * @return
     */
    public int minSubArrayLen(int target, int[] nums) {
        int n = nums.length;
        int minLength = Integer.MAX_VALUE;  // 用于存储最小子数组长度
        int sum = 0;  // 当前窗口的和
        int left = 0;  // 左指针

        for (int right = 0; right < n; right++) {
            // 扩展窗口,加入当前右指针的元素
            sum += nums[right];

            // 当窗口和大于等于 target 时,尝试缩小窗口
            // 因为要保证 sum >= target,所以可以同时保证 left 和 right 的位置相对正确。
            // 这是一种比较巧妙的解法。
            while (sum >= target) {
                // 更新最小长度
                minLength = Math.min(minLength, right - left + 1);

                // 缩小窗口,移除左端元素
                sum -= nums[left];
                left++;
            }
        }

        // 如果没有找到符合条件的子数组,返回 0
        return minLength == Integer.MAX_VALUE ? 0 : minLength;
    }

效果

1ms,超过 99.99%

小结

有时候双指针不太能很好的直接就想到。

性能的话还是不错的。

参考资料

https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/