前缀和解法
1.前缀和主要适用的场景是***原始数组不会被修改的情况下,频繁查询某个区间的累加和***
2.前缀和的核心代码:
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];
}
}
3.适用前缀和数组可以很快捷的求出原数组中某几个元素相加和,而不需要频繁的去遍历获取;但是由于需要创建一个新的数组,所以占用的空间比较大,这也是典型的***拿空间换时间***
4.算法题实例(稍微复杂点的应用):
(LeetCode.560)给你一个整数数组 nums 和一个整数 k ,请你统计并返回该数组中和为 k 的连续子数组的个数。
这个题目并不难,就算不使用前缀和解法,也可以用暴力破解去做;
class Solution {
public int subarraySum(int[] nums, int k) {
int res = 0;
for (int i = 0; i < nums.length; i++){
int sum = 0;
int j = i;
do {
sum += nums[j];
j++;
if (sum == k){
res++;
}
}while (j < nums.length);
}
return res;
}
}
暴力破解使用两个循环就可以解决,也可以使用前缀和的方法,现将所有的子数组都穷举出来,算它们的和;
int subarraySum(int[] nums, int k) {
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];
int res = 0;
// 穷举所有子数组
for (int i = 1; i <= n; i++)
for (int j = 0; j < i; j++)
// 子数组 nums[j..i-1] 的元素和
if (preSum[i] - preSum[j] == k)
res++;
return res;
}
仔细看这种解法就会发现,使用前缀和还不如不使用呢,声明了一个前缀和数组,还是需要通过双重遍历去实现,既增加了空间也没降低时间复杂度;这个写法确实是有这种问题,但是把思维扩展一点就可以想到,我们使用这种解法不一定是非要创建一个数组,也可以是map,list,set甚至是tree都可以,基于每种数据结构的特性选择出最合适的,一定程度上就能降低我们的时间复杂度;最后这种解法就是创建一个map,从而减少一层遍历;
class Solution {
public int subarraySum(int[] nums, int k) {
// map:前缀和 -> 前缀和出现的次数
HashMap<Integer, Integer> map = new HashMap<>();
map.put(0,1);
int res = 0;
int sum0_i = 0;
for (int i = 0; i < nums.length; i++){
sum0_i += nums[i];
int sum0_j = sum0_i - k;
if (map.containsKey(sum0_j)){
res += map.get(sum0_j);
}
map.put(sum0_i, map.getOrDefault(sum0_i, 0)+1);
}
return res;
}
}
5.思考:前缀和解法就是使用空间换时间,单纯的做算法联系的时候我们更多追求的是空间和时间两个层面上的最优解,但是在工作中空间换时间的这种思路可能使用的更多,各个互联网大厂更愿意的是通过对空间的使用从而换来用户们更好的体验。