前缀和 也是一个 常用的技巧。
但是理解止步于概念,在实战中不习惯用到,这里进行专项突破。
套路总结:
对于数组:nums (长度 n)
对应前缀和数组:preSums (长度为n+1), preSums[i] 表示前 i 个 元素的和。
例如:
preSums[0] 表示 前 0个元素的和
preSums[1] 表示 前 1个元素的和, 即 nums[0]
preSums[2] 表示 前 2个元素的和, 即 nums[0] + nums[1]

preSums[n] 表示 前 n个元素的和, 即 nums[0] + nums[1] + … + nums[n - 1]
这样规定 preSums 的好处是,preSums[j] - preSums[i] ,当 i = 0,就可方便表示 前 j 个数的和。

  1. // 初始条件:
  2. const preSums = [0];
  3. let map = new Map(); // key 为前缀和,value 为 出现次数
  4. map.set(0, 1); // 前缀和为0,即 preSums[0],的次数为 1.为什么是0呢?因为加法中,0 没有副作用
  5. // 计算前缀和
  6. for (let j = 0; j < n; j++) {
  7. preSums[j + 1] = preSums[j] + nums[j];
  8. map.set(preSums[j + 1], (map.get(preSums[j + 1]) || 0) + 1); // 无脑记录就好
  9. }

724. 寻找数组的中心下标

我这里写的有点特殊,前缀和数组sums 第一项不填0,而是 sums[i] 代表:nums[0…i] 的和

  1. var pivotIndex = function(nums) {
  2. let sums = []; // 方便计算 sums[j] - sums[i - 1] 表示 i...j 的和
  3. let sum = 0;
  4. for (let num of nums) {
  5. sum += num;
  6. sums.push(sum);
  7. }
  8. let n = sums.length;
  9. for (let i = 0; i < n; i++) {
  10. let leftSum = sums[i - 1] || 0;
  11. let rightSum = sums[n - 1] - sums[i]
  12. if (leftSum === rightSum) {
  13. return i;
  14. }
  15. }
  16. return -1;
  17. };

560. 和为 K 的子数组 930. 和相同的二元子数组 (两个题目重复)

一开始有思维定式,就是先生成完整的 前缀和数组,然后再谋划下一步,其实,有些技术在生成前缀树的过程做是最好的:
先说下流程:
1)找子数组,下标为 [i,…,j]
2) 我们很容易就知道,子数组和 = sums[j] - sums[i] sums是前缀和数组
3) 这样我们就可以遍历一次前缀和,看 是否存在这样的组合 sums[i] 和 sums[j]

  1. var subarraySum = function(nums, k) {
  2. let sums = []; // sums[i] 代表:nums[0...i] 的和
  3. let sum = 0;
  4. for (let num of nums) {
  5. sum += num;
  6. sums.push(sum);
  7. }
  8. let count = 0;
  9. let n = sums.length;
  10. for (let i = 0; i <= n; i++) {
  11. p_j = sums[i] + k; // j 的下标还得 比 i 大
  12. if (sums.includes(p_j)) {
  13. if (sums.indexOf(p_j) >= i) {
  14. count++;
  15. }
  16. }
  17. }
  18. return count;
  19. };

于是我们写了上面的代码,但是发现了我们不好解决的问题,不光是 sums.includes(p_j) 的问题,还要保证两个问题:1)i > j;2)还要考虑前缀和里重复的问题。这样的话,我们这套思路就需要优化了。
为保证 i > j, 其实我们可以从 构建 前缀和数组 数组思考:前缀和的定义为 sums[j] = nums[0….j-1],每次变化的是结尾 j 的值,比 j 大的我们还没有遍历到,所以我们可以保证了。
重复的问题,我们可以通过 map 的形式 也能搞定。
所以,我们再写代码:

  1. var subarraySum = function(nums, k) { // 子数组不是子序列
  2. const n = nums.length;
  3. if (n === 1) {
  4. return nums[0] === k ? 1 : 0;
  5. }
  6. const preSums = [0];
  7. let map = new Map();
  8. map.set(0, 1);
  9. let count = 0;
  10. for (let j = 0; j < n; j++) {
  11. preSums[j + 1] = preSums[j] + nums[j];
  12. let targetLeft = preSums[j + 1] - k;
  13. if (map.has(targetLeft)) {
  14. count += map.get(targetLeft);
  15. }
  16. map.set(preSums[j + 1], (map.get(preSums[j + 1]) || 0) + 1); // 无脑记录就好
  17. }
  18. return count;
  19. };

1248. 统计「优美子数组」

和 560 一模一样哈,但是不同的是,这道题不是 求和,而是求 奇数的个数

  1. var numberOfSubarrays = function(nums, k) {
  2. let n = nums.length;
  3. let countArr = [0];
  4. let map = new Map();
  5. map.set(0, 1); // [0]:0 个 奇数,初始有1个
  6. let count = 0;
  7. for (let i = 0; i < n; i++) {
  8. if (nums[i] % 2 === 1) { // yes
  9. countArr[i + 1] = countArr[i] + 1;
  10. } else {
  11. countArr[i + 1] = countArr[i];
  12. }
  13. let targetLeft = countArr[i + 1] - k;
  14. if (map.has(targetLeft)) {
  15. count += map.get(targetLeft);
  16. }
  17. map.set(countArr[i + 1], (map.get(countArr[i + 1]) || 0) + 1);
  18. }
  19. return count;
  20. };

974. 和可被 K 整除的子数组

一样的思路,只是在 一个数的同余时,有些问题

  1. var subarraysDivByK = function(nums, k) {
  2. let n = nums.length;
  3. let preSums = [0];
  4. let map = new Map(); // key 为 (前缀和 % k), value 依然是 个数
  5. map.set(0, 1);
  6. let count = 0;
  7. for (let i = 0; i < n; i++) {
  8. preSums[i + 1] = preSums[i] + nums[i];
  9. let remainder = (preSums[i + 1] % k + k) % k; // 3 k !!!
  10. if (map.has(remainder)) {
  11. count += map.get(remainder);
  12. }
  13. map.set(remainder, (map.get(remainder) || 0) + 1);
  14. }
  15. return count;
  16. };

523. 连续的子数组和

还是和 974 相同的思路,但是需要调整细节,只是要求 子数组必须至少有两个数!!

  1. var checkSubarraySum = function(nums, k) {
  2. let n = nums.length;
  3. let preSums = [0];
  4. let map = new Map(); // key 为 前缀和,value 为 下标组成的数组
  5. map.set(0, [-1]);
  6. for (let i = 0; i < n; i++) {
  7. preSums[i + 1] = preSums[i] + nums[i];
  8. let remainder = (preSums[i + 1] % k + k) % k;
  9. let indexList = map.get(remainder) || [];
  10. if (map.has(remainder)) {
  11. let has = indexList.filter((v) => (i - v >= 2)).length > 0; // 多的判断条件
  12. if (has) {
  13. return true;
  14. }
  15. }
  16. indexList.push(i);
  17. map.set(remainder, indexList);
  18. }
  19. return false;
  20. };

小结:

看到 子数组 连续 和,就要想到这样的思路