目录
前缀和
从「一维数组的动态和」说起
前缀和,英文是 preSum,是一种基础算法。
为了了解背景,咱们先看 LeetCode 上一个简单题目:1480. 一维数组的动态和
这个题让我们求 runningSum[i] = sum(nums[0]…nums[i]),如果你没有了解过「前缀和」,可能会写出两重循环:每个 runningSum[i],累加从 0 位置到 i 位置的 nums[i]。即,写出下面的代码:
int* runningSum(int* nums, int numsSize, int* returnSize){
int* preSum = malloc(sizeof(int) * numsSize);
*returnSize = numsSize;
for (int i = 0; i < numsSize; ++i) {
int sum = 0;
for (int j = 0; j <= i; ++j) {
sum += nums[j];
}
preSum[i] = sum;
}
return preSum;
}
两重循环的时间复杂度是 O(n*2),效率比较低。
其实我们只要稍微转变一下思路,就发现没必要用两重循环。 当已经求出
runningSum[i] = sum(nums[0]…nums[i])
, 那么runningSum[i + 1] = sum(nums[0]…nums[i]) + nums[i + 1]
= runningSum[i] + nums[i + 1]
。
int* runningSum(int* nums, int numsSize, int* returnSize){
int* preSum = malloc(sizeof(int) * numsSize);
*returnSize = numsSize;
for (int i = 0; i < numsSize; ++i) {
if (i == 0) {
preSum[i] = nums[i];
} else {
preSum[i] = preSum[i - 1] + nums[i];
}
}
return preSum;
}
前缀和是什么
通过上面的内容,很容易理解preSum 数组其实就是「前缀和」。
「前缀和」 是从 nums 数组中的第 0 位置开始累加,到第 i 位置的累加结果,我们常把这个结果保存到数组 preSum 中,记为 preSum[i]。
在前面计算「前缀和」的代码中,计算公式为 preSum[i] = preSum[i - 1] + nums[i] ,为了防止当 i = 0 的时候数组越界,所以加了个 if (i == 0) 的判断,即 i == 0 时让 preSum[i] = nums[i]。
在其他常见的写法中,为了省去这个 if 判断,我们常常把「前缀和」数组 preSum 的长度定义为原数组长度+1。preSum 的第 0 个位置,相当于一个占位符,置为 0。 那么就可以把 preSum 的公式统一为 preSum[i] = preSum[i - 1] + nums[i - 1],此时的 preSum[i] 表示 nums 中 iii 元素左边所有元素之和(不包含当前元素 iii)。
下面以 [1, 12, -5, -6, 50, 3] 为例,用动图讲解一下如何求 preSum。
上图表示:
preSum[0] = 0;
preSum[1] = preSum[0] + nums[0];
preSum[2] = preSum[1] + nums[1];
...
前缀和有什么用
用途一:求数组前 i 个数之和
求数组前 i 个数之和,是「前缀和」数组的定义,所以是最基本的用法。
如果要求 nums 数组中的前 2 个数的和(即 sum(nums[0], nums[1])) ,直接返回 preSum[2] 即可。 同理,如果要求 nums 数组中所有元素的和(即 sum(nums[0]..nums[length−1])),直接返回 preSum[length] 即可。
用途二:求数组的区间和
利用 preSum 数组,可以在 O(1) 的时间内快速求出 nums 任意区间 [i,j](两端都包含) 内的所有元素之和。
公式为: 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] 的所有数字之和。
接下来我们以 643. 子数组最大平均数 I 为例,这个题目要求数组nums中所有长度为k的连续子数组中的最大的平均数。
这个题可以用「前缀和」来解决,也可以用固定大小为 k 的「滑动窗口」来解决。
要求大小为 k 的窗口内的最大平均数,可以求 [i, i + k] 区间的最大「和」再除以 k,即要求 (preSum[i] - preSum[i - k]) / k 的最大值。
总之,如果题目要求「区间和」的时候,那么就可以考虑使用「前缀和」。
double findMaxAverage(int* nums, int numsSize, int k) {
double preSum[numsSize + 1];
preSum[0] = 0;
double max = INT_MIN;
for (int i = 1; i <= numsSize; ++i) {
preSum[i] = preSum[i - 1] + nums[i - 1];
if(i - k >= 0