算法 系列 前缀

基础数据结构...

Posted by lichao modified on June 20, 2020

前缀和不难,却很有⽤,主要⽤于处理数组区间的问题。

algorithm

前缀和的思路是这样的,对于⼀个给定的数组 nums ,我们额外开辟⼀个前缀和数组进⾏预处理:

1
2
3
4
5
6
int n = nums.length;
// 前缀和数组
int[] preSum = new int[n + 1];
preSum[0] = 0;
for (int i = 0; i < n; i++)
  preSum[i + 1] = preSum[i] + nums[i];

这个前缀和数组 preSum 的含义也很好理解, preSum[i] 就是 nums[0..i-1] 的和。那么如果我们想求 nums[i..j] 的和,只需要⼀步操作preSum[j+1]-preSum[i] 即可,⽽不需要重新去遍历数组了。

回到这个⼦数组问题,我们想求有多少个⼦数组的和为 k,借助前缀和技巧很容易写出⼀个解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  int subarraySum(int[] nums, int k) {
    int n = nums.length;
    // 构造前缀和
    int[] sum = new int[n + 1];
    sum[0] = 0;
    for (int i = 0; i < n; i++)
      sum[i + 1] = sum[i] + nums[i];
    int ans = 0;
    // 穷举所有⼦数组
    for (int i = 1; i <= n; i++)
      for (int j = 0; j < i; j++)
      // sum of nums[j..i-1]
      if (sum[i] - sum[j] == k)
        ans++;
    return ans;
  }

这个解法的时间复杂度 O(N^2) 空间复杂度 O(N),并不是最优的解法。

优化的思路是:直接记录下有⼏个 sum[j]sum[i] - k 相等,直接更新结果,就避免了内层的 for 循环。我们可以⽤哈希表,在记录前缀和的同时记录该前缀和出现的次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  int subarraySum(int[] nums, int k) {
    int n = nums.length;
    // map:前缀和 -> 该前缀和出现的次数
    HashMap<Integer, Integer>
    preSum = new HashMap<>();
    // base case
    preSum.put(0, 1);
    int ans = 0, sum0_i = 0;
    for (int i = 0; i < n; i++) {
      sum0_i += nums[i];
      // 这是我们想找的前缀和 nums[0..j]
      int sum0_j = sum0_i - k;
      // 如果前⾯有这个前缀和,则直接更新答案
      if (preSum.containsKey(sum0_j))
        ans += preSum.get(sum0_j);
      // 把前缀和 nums[0..i] 加⼊并记录出现次数
      preSum.put(sum0_i,
      preSum.getOrDefault(sum0_i, 0) + 1);
    }
    return ans;
  }

这样,就把时间复杂度降到了 O(N),是最优解法了。