LeetCode 560. 和为 K 的子数组 题解,理解前缀和思想。

题目

https://leetcode.cn/problems/subarray-sum-equals-k/

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。

题解

粗暴解

最直观的当然是先想一个粗暴解:子数组要求是连续的,所以我们直接遍历数组,每轮定一个起点,内部另起一个终点的循环,求和判断:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public int subarraySumBruteForce(int[] nums, int k) {
    // 暴力解
    int cnt = 0;

    for (int i = 0; i < nums.length; i++) {
        // 逐个遍历原数组
        int sum = 0;

        for (int j = i; j < nums.length; j++) {
            // 逐个遍历i之后的数据,求和
            sum += nums[j];
            if (sum == k) {
                ++cnt;
            }
        }
    }
    return cnt;
}

看看leetcode的运行反馈:

执行用时: 1760 ms , 在所有 Java 提交中击败了 5.05% 的用户 内存消耗: 45.1 MB , 在所有 Java 提交中击败了 6.51% 的用户 通过测试用例: 92 / 92

循环套循环,时间复杂度O(n^2),有没有优化空间?

前缀和

前缀和是本题粗暴解的改善方案。

前缀和是一种预计算,可用来加速我们的程序,涉及频繁寻址的场景还可以考虑存到哈希表中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static int subarraySumPreSum(int[] nums, int k) {
    // 前缀和
    int cnt = 0;

    int[] preSums = new int[nums.length];
    // 首次遍历,生成前缀和数组
    int preSum = 0;
    for (int i = 0; i < nums.length; i++) {
        // 计算前缀和
        preSum += nums[i];
        preSums[i] = preSum;
    }

    // 再遍历前缀和数组,做k等值判断
    for (int left = 0; left < preSums.length; left++) {
        // 外层循环表示左遍历指针,左遍历指针在一轮遍历中固定不变,移动右遍历指针
        int sum;
        for (int right = left; right < preSums.length; right++) {
            sum = right == left ? preSums[right] : preSums[right] - preSums[left];
            if (sum == k) {
                // 只要右区间减去左区间等值,说明满足子数组和为k,同时继续遍历不跳出本轮循环,因为可能有的值为负数,子数组继续增长也有可能满足条件
                ++cnt;
            }
        }
    }

    return cnt;
}

经过O(n)的一轮循环,我们预计算出preSum数组。在下面的子数组和判断中,我们的和计算其实没有本质上的提升。

看看leetcode反馈:

提交结果: 执行用时: 1906 ms , 在所有 Java 提交中击败了 5.05% 的用户 内存消耗: 43.1 MB , 在所有 Java 提交中击败了 90.65% 的用户 通过测试用例: 90 / 90

时间复杂度依然是O(n^2),还需要优化!前面讲前缀和概念的时候提到了,需要频繁寻址查询的时候,可以考虑缓存前缀和。

前缀和缓存

我们内部的一轮循环目的是逐个判断是否有前缀和的差是否有等于k的。

直接将前缀和丢进map,顺便,我们在计算前缀和的一轮循环中就可以根据缓存拿到前序所有前缀和,当前前缀和 - 某个前缀和如果等于k,说明对应一个子数组的和为k,所以代码里直接判断map.containsKey(preSum-k)即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public static int subarraySumPreSumMap(int[] nums, int k) {
    int cnt = 0;
    // 未开始遍历数组时,前缀和为0
    int preSum = 0;
    // 使用map存 前缀和->出现次数 的映射;
    Map<Integer, Integer> preSum2Cnt = new HashMap<>(MapUtils.capacity(nums.length));
    preSum2Cnt.put(preSum, 1);

    for (int num : nums) {
        preSum += num;
        // 前缀和-k == 当前位置i元素和减去目标值后的和,
        // 如果这个差值在前面的前缀和中存在,说明子数组和==k,即满足题目条件;
        cnt += preSum2Cnt.getOrDefault(preSum - k, 0);

        // 先判断当前位置-k的前缀和在map中是否已存在;再将当前前缀和放入map;
        preSum2Cnt.put(preSum, preSum2Cnt.getOrDefault(preSum, 0) + 1);
    }

    return cnt;
}

时间复杂度直降为O(n)

  • 执行结果: * 通过 * 显示详情 * 添加备注 * * 执行用时: * 20 ms * , 在所有 Java 提交中击败了 * 94.14% * 的用户 * 内存消耗: * 43.8 MB * , 在所有 Java 提交中击败了 * 75.22% * 的用户 * 通过测试用例: * 90 / 90

代码

https://github.com/redolog/algorithm-java/blob/main/src/main/java/com/algorithm/dataStructure/array/SubArraySumEqualsK.java