要求以 i 结尾和为 k 的连续子数组个数,要从下标范围 0…i 中统计出符合条件的下标 j 的个数,其中 0 <= i <= j,使得下标范围 j…i 的子数组的和为 k 。

思路分析完,采用暴力解法。定义外层循环慢指针 slow,再定义一个内循环快指针 fast 从当前慢指针 slow 开始向前遍历,统计所有符合 k 的子数组。

代码如下:

class Solution {
    public int subarraySum(int[] nums, int k) {
        
        int count = 0;
        for(int slow = 0; slow < nums.length; slow++){
            int fast = slow;
            int result = 0;
            while(fast >= 0){
                result += nums[fast];
                if(result == k){
                    count++;
                }
                fast--;
            }
        }
        return count;

    }
}

很明显,暴力解法时间复杂度为 O(n2)。

优化方法:前缀和 + 哈希表

暴力解法最大的一个问题就是我们遍历 慢指针 slow 所指的元素时,要把小于它的所有下标都要计算一遍数字和来判断是否符合条件。这就是 O(n2) 时间复杂度的来源。

为了后续分析方便,这里不再称快慢指针,而是用 i 和 j 表示。

定义 pre[i] 是 [0, …, i] 里所有数的和,那么 pre[i] 可以由 pre[i – 1] 递推求出:

pre[i] = pre[i – 1] + nums[i];

那么将 pre[i – 1] 这一项移到左侧得到的式子代表什么含义呢?

pre[i] – pre[i – 1] = nums[i];

是不是就是说 [0, …, i] 所有数的和 减去 [0, …, i – 1] 所有数的和的结果是 nums[i] 。那么我们为了求出符合条件的和为 k 的子数组,可以取 [0, …, i] 所有数的和 减去 [0, …, j] 所有数的和 作为判断条件,其中 j <= i。 是的,这就是优化方案。

if(pre[i] – pre[j – 1] == k)

整理一下就是 pre[j – 1] == pre[i] – k 作为判断条件来统计个数。

此时 if 判断逻辑变为:

if (pre[j – 1] == pre[i] – k) {}

思路有了,开始总结一下我们用到了哪些知识点:

前缀和:

对于数组中的任何位置 i ,前缀和 pre[i] 是数组中从第一个元素到第 i 个元素的总和。这意味着如果我们想知道从元素 j 到 i 的子数组的和,可以用 pre[i] – pre[ j – 1] 来计算。

Map 来存储数据:

代码实现中,采用 哈希表来存储每个前缀和出现的次数。通过上面分析的 if 判断条件来快速查找某个符合条件的前缀和是否已经存在,以及它出现的次数。

完整代码:

class Solution {
    public int subarraySum(int[] nums, int k) {
        
        int count = 0; 
        int pre = 0;
        HashMap<Integer, Integer> preMap = new HashMap<>();
        preMap.put(0, 1);
        for(int i = 0; i < nums.length; i++){
            pre += nums[i];
            if(preMap.containsKey(pre - k)){
                count += preMap.get(pre - k);
            }
            preMap.put(pre, preMap.getOrDefault(pre, 0) + 1);
        }
        return count;
    }
}

这里有个小细节,为什么会有 preMap.put(0, 1);这行代码,什么含义?

其实我们在统计前缀和的时候,在首次找到子数组和为 k 的时候,pre[i] – k 是 0,如果不加入这行代码,那么就会丢失这首次查到的结果。所以这行代码本意是初始化的作用,记录第一次找到符合结果的子数组。

遍历数组的时候时间复杂度为 O(n),但是在查找符合条件的子数组的时候是基于 Map 的数据结构,时间复杂度为 O(1),因此整体的时间复杂度为 O(n)。

Categories:

Tags:

No responses yet

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注