1.定义
一维数组:前缀和(Prefix Sum)的定义为:对于一个给定的数列 A, 它的前缀和数列 S 是通过递推能求出来得 部分和。
Leetcode上的一道题目:
**
**
//写法一:
public int[] runningSum(int[] nums) {
int[] preNum = new int[nums.length];
for(int i = 0; i < nums.length;i++){
if(i == 0){
preNum[i] = nums[i];
}else{
preNum[i] = nums[i] + preNum[i - 1];
}
}
return preNum;
}
//写法二:
public int[] runningSum(int[] nums) {
int[] preSum = new int[nums.length + 1];
for(int i = 0; i < nums.length;i++){
preSum[i + 1] = nums[i] + preSum[i];
}
return preSum;
}
2.用途
用途一:求数组前i个数之和
求数组前i个数之和,是「前缀和」数组的定义,所以是最基本的用法。 举例而言:
- 如果要求
nums
数组中的前 2 个数的和,直接返回preNum[2]即可。 针对写法二 ,写法一为(preNum[1]) - 同理,如果要求
nums
数组中所有元素的和,直接返回 preSum[length](数组的长度)即可。
用途二:求数组的区间和
利用 preSum
数组,可以在 o(1)的时间内快速求出 nums
任意区间 [i,j]内的所有元素之和。
公式为:
sum(i,j) = preSum[j] - preSum[i - 1] (针对写法一)
sum(i,j) = preSum[j +1] - preSum[i] (针对写法二)
原理:其实就是消除公共部分即 0~i-1
部分的和,那么就能得到 i~j
部分的区间和。
注意写法二中,使用的是 preSum[j + 1]
和 preSum[i]
,需要理解为什么这么做。
preSum[j + 1]
表示的是nums
数组中 [0,j]的所有数字之和(包含0和 j)。preSum[i]
表示的是nums
数组中 [0,i - 1]的所有数字之和(包含0 和 i- 1)。- 当两者相减时,结果留下了
nums
数组中 [i,j]的所有数字之和。
class NumArray {
private int[] preSum;
public NumArray(int[] nums) {
int n = nums.length;
preSum = new int[n + 1];
for(int i = 0;i < n;i++){
preSum[i + 1] = nums[i] + preSum[i];
}
}
public int sumRange(int left, int right) {
return preSum[right + 1] - preSum[left];
}
}
-
空间复杂度:定义「前缀和」数组,需要 n + 1的空间,所以空间复杂度是O(N)
-
时间复杂度:
-
- 初始化「前缀和」数组,需要把数组遍历一次,时间复杂度是 O(N)
- 求[i,j]范围内的区间和,只用访问
preSum[j + 1]
和preSum[i]
,时间复杂度是O(1)
用途3:和为k的子数组
class Solution {
public int subarraySum(int[] nums, int k) {
HashMap<Integer, Integer> map = new HashMap<>();
int count = 0;
int[] array = new int[nums.length + 1];
for (int i = 1; i <= nums.length; i++) {
array[i] = array[i - 1] + nums[i - 1];
if (array[i] == k) {
count++;
}
// 用map记录所有前缀和,代替使用for循环判断
if (map.containsKey(array[i] - k)) {
count += map.get(array[i] - k);
}
map.put(array[i], map.getOrDefault(array[i], 0) + 1);
}
return count;
}
}
拓展:二维矩阵的「前缀和」
步骤一:求 preSum
我们定义 preSum[i][j]
表示 从 [0,0]
位置到 [i,j]
位置的子矩形所有元素之和。
如果求 preSum[i][j]
的递推公式为:
nums[i + 1][j + 1] = nums[i][j + 1] + nums[i + 1][j] - nums[i][j] + matrix[i][j];
可以用下图帮助理解:
步骤二:根据 preSum 求子矩形面积
前面已经求出了数组中从 [0,0]
位置到 [i,j]
位置的 preSum
。
如果要求 [row1, col1]
到 [row2, col2]
的子矩形的面积的话,用 preSum
计算时对应了以下的递推公式:
nums[row2 + 1][col2 + 1] - nums[row1][col2 + 1] - nums[row2 + 1][col1] + nums[row1][col1];
同样利用一张图来说明:
class NumMatrix {
int[][] nums;
public NumMatrix(int[][] matrix) {
int m = matrix.length;
if(m > 0){
int n = matrix[0].length;
nums = new int[m + 1][n + 1];
for(int i = 0;i < m;i++){
for(int j = 0;j < n;j++){
nums[i + 1][j + 1] = nums[i][j + 1] + nums[i + 1][j] - nums[i][j] + matrix[i][j];
}
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
return nums[row2 + 1][col2 + 1] - nums[row1][col2 + 1] - nums[row2 + 1][col1] + nums[row1][col1];
}
}
总结
前缀和的复杂度:
1.空间复杂度是O(n)
2. 初始化的时间复杂度是 O(n)
3. 求「区间和」的时间复杂度是O(1)
参考:https://zhuanlan.zhihu.com/p/407341747
3.差分数组
前缀和主要适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和。
差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。
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]; }
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]; }
这样修改数组某个区间
[i,j]
时,只需要修改diff[i]
和diff[j+1]
的值,再反推结果数组