labuladong 算法

一、前缀和技巧

前缀和主要适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和。

前缀和技巧的思想不难,也没有任何难理解的地方。主要是在实际的问题中要想到使用这个技巧来解答,其核心代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class PrefixSum {
    // 前缀和数组
    private int[] prefix;
    
    /* 输入一个数组,构造前缀和 */
    public PrefixSum(int[] nums) {
        prefix = new int[nums.length + 1];
        // 计算 nums 的累加和
        for (int i = 1; i < prefix.length; i++) {
            prefix[i] = prefix[i - 1] + nums[i - 1];
        }
    }

    /* 查询闭区间 [i, j] 的累加和 */
    public int query(int i, int j) {
        return prefix[j + 1] - prefix[i];
    }
}

304. 二维区域和检索 - 矩阵不可变

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class NumMatrix {
    int[][] matSum;
    public NumMatrix(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        matSum = new int[m+1][n+1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                matSum[i][j] = matSum[i-1][j] + matSum[i][j-1] + matrix[i-1][j-1] - matSum[i-1][j-1];
            }
        }
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        return matSum[row2+1][col2+1] - matSum[row1][col2+1] - matSum[row2+1][col1] + matSum[row1][col1];
    }
}

前缀和+哈希表优化

560. 和为 K 的子数组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Solution {
    public int subarraySum(int[] nums, int k) {
        int res = 0, sum = 0;
        HashMap<Integer, Integer> map = new HashMap<>();
        map.put(0, 1);
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
            if (map.containsKey(sum - k)) {
                res += map.get(sum - k);
            }
            map.put(sum, map.getOrDefault(sum, 0) + 1);
        }
        return res;
    }
}

二、差分数组技巧

与前缀和的思想类似,也是通过额外数组来进行原数组的表示。区别是差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减

比如说,我给你输入一个数组 nums,然后又要求给区间 nums[2..6] 全部加 1,再给 nums[3..9] 全部减 3,再给 nums[0..4] 全部加 2,再给…

一通操作猛如虎,然后问你,最后 nums 数组的值是什么?

常规的思路很容易,你让我给区间 nums[i..j] 加上 val,那我就一个 for 循环给它们都加上呗,还能咋样?这种思路的时间复杂度是 O(N),由于这个场景下对 nums 的修改非常频繁,所以效率会很低下。

这里就需要差分数组的技巧,类似前缀和技巧构造的 prefix 数组,我们先对 nums 数组构造一个 diff 差分数组,diff[i] 就是 nums[i]nums[i-1] 之差:直接通过这个diff差分数组就可以反推出原数组。

1
2
3
4
5
6
int[] diff = new int[nums.length];
// 构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
    diff[i] = nums[i] - nums[i - 1];
}

现在我们把差分数组抽象成一个类,包含 increment 方法和 result 方法:

 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
29
30
31
32
33
34
35
36
// 差分数组工具类
class Difference {
    // 差分数组
    private int[] diff;
    
    /* 输入一个初始数组,区间操作将在这个数组上进行 */
    public Difference(int[] nums) {
        assert nums.length > 0;
        diff = new int[nums.length];
        // 根据初始数组构造差分数组
        diff[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
            diff[i] = nums[i] - nums[i - 1];
        }
    }

    /* 给闭区间 [i,j] 增加 val(可以是负数)*/
    public void increment(int i, int j, int val) {
        diff[i] += val;
        if (j + 1 < diff.length) {
            diff[j + 1] -= val;
        }
    }

    /* 返回结果数组 */
    public int[] result() {
        int[] res = new int[diff.length];
        // 根据差分数组构造结果数组
        res[0] = diff[0];
        for (int i = 1; i < diff.length; i++) {
            res[i] = res[i - 1] + diff[i];
        }
        return res;
    }
}

370 题「区间加法」

img