算法技巧之前缀和+哈希表

208 阅读2分钟

1. 前缀和是什么

对于一个数组numsnums,若其前缀和数组为preSumpreSum ,则有:

preSum[i]=x=0inums[x]=preSum[i1]+nums[i]preSum[i] = \sum^{i}_{x=0}{nums[x]} = preSum[i - 1] + nums[i]

由前缀和特点,我们可以推导:

x=ijnums[x]=preSum[j]preSum[i1]\sum^{j}_{x=i}{nums[x]} = preSum[j] - preSum[i-1]

特殊的一点是:

x=0jnums[x]=preSum[j]\sum^{j}_{x=0}{nums[x]} = preSum[j]

利用前缀和,可以快速解决一下连续区间计算问题,而再配上哈希表,则有希望进一步降低时间复杂度。

2. 小实牛刀

题目描述:

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

LeetCode 560

如果使用模拟法,也就是挨个计算数组中不同区间中数字之和,统计和为 k的数量,则时间复杂度为O(n2)O(n^2)

下面我们尝试使用前缀和来优化,根据题目,我们需要找出数组中和为 k 的连续子数组,即:

x=ijnums[x]=k\sum^{j}_{x=i}{nums[x]} = k

我们使用前缀和来进一步替换:

x=ijnums[x]=preSum[j]preSum[i1]=k\sum^{j}_{x=i}{nums[x]} = preSum[j] - preSum[i-1] = k

也就是说,前缀和数组中,后项减前项差值为k的各组符合题意,那么我们可以首先计算前缀和数组,然后依次求其中两项差值,差为k的个数,即为题解。这种遍历算法很常见,其时间复杂度为仍为O(n2)O(n^2),与原算法相比,虽然使用前缀和避免了频繁的累加,但是时间复杂度仍然没有量级上的变化。

我们进一步思考,既然题目仅统计个数,那我们可以先把前缀和不同值的个数统计出来,然后找出他们差值为k的一组,就能直接计算出计算出个数,避免遍历。存储值的数量,容易想到哈希表。需要注意的是,差为k,必须是后项减前项,那我们可以一边计算前缀和,一边统计数量,避免后面的前缀和前介入运算。至此,算法轮廓基本完成,伪代码如下:

for (int num : nums) {
    计算到当前项的前缀和
    根据当前前缀和,查表找出满足差为k的前缀和数量,总数量累加该数量
    更新哈希表,将当前前缀和存入
}
返回总数量

需要注意的是,每一项自身也算一个区间,例如[i,i],因此为了能计算到自身这种区间,哈希表默认需要存入(0,1)的一项。

具体代码实现如下:

public int subarraySum(int[] nums, int k) {
    int result = 0;
    int preSum = 0;
    Map<Integer, Integer> cache = new HashMap<>();
    cache.put(0, 1);
    for (int num : nums) {
        preSum += num;
        result += cache.getOrDefault(preSum - k, 0);
        cache.compute(preSum, new BiFunction<Integer, Integer, Integer>() {
            @Override
            public Integer apply(Integer k, Integer v) {
                return null == v ? 1 : v + 1;
            }
        });
    }
    return result;
}

3. 总结

若题目中出现子数组,并给出连续子数组和限制条件,以求解,或有其他可分解成该类问题的其他条件,则应考虑前缀和,如果题目中仅要求统计数量,可使用哈希表进行优化。

如果你看到最后,欢迎点个赞。