[前缀和-动态规划] 560. 和为K的子数组(前缀和法 → 空间优化 → 前缀和+哈希表)
560. 和为K的子数组
题目链接:https://leetcode-cn.com/problems/subarray-sum-equals-k/
分类:动态规划(前缀和)、哈希表
.
.
什么是前缀和?
参考链接:前缀和 - by YocnZhao
一维前缀和
设置一个数组num和它的前缀和数组sum,他们满足以下公式:
sum[0] = num[0]
sum[1] = num[0] + num[1]
sum[2] = num[0] + num[1] + num[2]
...
即 sum[n]=num[1]+num[2]+...+num[n]。
一维前缀和的构造代码:
public int[] oneDimen(int[] src) {
int[] result = new int[src.length];
for (int i = 0; i < result.length; i++) {
if (i == 0) {
result[i] = src[i];
} else {
result[i] = result[i - 1] + src[i];
}
}
return result;
}
二维前缀和
如下图:
使用一个二维数组prefixSum来保存前缀和,数组里的每一个位置表示原数组当前位置的左上方矩形的元素之和。
二维前缀和数组的构造需要分四种情况讨论:
- i == 0 && j == 0,直接赋值:prefixSum[0, 0] = src[0, 0];
- i == 0,最左边的一排,图中黄色部分,按垂直方向求和:prefixSum[0, j] = prefixSum[0, j-1] + src[0, j];
- j == 0,最上面一排,图中红色部分,按水平方向求和:prefixSum[i, 0] = prefixSum[i-1, 0] + src[i, 0];
- i !=0 || j != 0,图中绿色部分,prefixSum[i][j] = prefixSum[i - 1][j] + prefixSum[i][j - 1] + src[i][j] - prefixSum[i - 1][j - 1];
第4步中,绿色部分位置的前缀和等于以它为右下角的矩形的元素之和,我们不需要枚举矩形内的元素重新计算,可以利用现有的计算结果,如下图:
要计算prefixSum[2,2],就等于src[2,2]加上绿色框,加上红色框,再减去它们重叠的部分,其中绿色框的前缀和保存在prefixSum[2,1],红色框的前缀和保存在prefixSum[1,2],两个矩形的重叠部分前缀和保存在prefixSum[1,1],所以:
prefixSum[2,2] = prefixSum[1, 2] + prefixSum[2, 1] - prefixSum[1, 1] + src[2, 2];//注意:最后src[2,2]容易遗漏
二维前缀和的构造代码:
public int[][] twoDimen(int[][] nums){
int rows = nums.length, cols = nums[0].length;
int[][] sum = new int[rows][cols];
for(int i = 0; i < rows; i++){
for(int j = 0; j < cols; j++){
//矩阵第0个元素
if(i == 0 && j == 0) sum[i][j] = nums[i][j];
//矩阵第0行
else if(i == 0) sum[i][j] = sum[i][j-1]+nums[i][j];
//矩阵第0列
else if(j == 0) sum[i][j] = sum[i-1][j]+nums[i][j];
//矩阵其他位置
else{
sum[i][j] = sum[i-1][j] + sum[i][j-1] + nums[i][j] - sum[i-1][j-1];
}
}
}
return sum;
}
前缀和的应用(*****)
- 计算连续子数组和:
- 计算百分比:面试常考题,见【朝夕的ACM笔记】算法基础-前缀和 = 按分数做桶排序 + 对桶构造前缀和
- 圆环切割:分割环(前缀和 + 哈希) = 判断序列上是否存在和为总和一半的连续子序列
思路1:前缀和(O(N^2),O(N))
1、设置一个数组sum来保存nums[0,i]的和,即保存前缀和,其中i=0~n-1,则前缀和的计算公式为:
nums[i~j]的和=sum[j+1]-sum[i],
例如:
nums=[1,1,1]
sum=[0,1,2,3]
则nums[0,1]的和=sum[2]-sum[0]=2
2、得到sum数组后,开始统计数组中和为k的连续子数组个数,对sum做双重遍历:
外层for-i从下标0开始,内层for-j从下标i+1开始,nums[i~j]的和=sum[j]-sum[i],如果和等于k,则计数器ret+1。
3、返回ret存放的最终结果。
实现代码:
class Solution {
public int subarraySum(int[] nums, int k) {
int len = nums.length;
int[] sum = new int[len + 1];
//构造前缀和数组
for(int i = 1; i < len + 1; i++){
sum[i] = sum[i - 1] + nums[i - 1];
}
int ret = 0;
for(int i = 0; i < sum.length; i++){
for(int j = i + 1; j < sum.length; j++){
if(sum[j] - sum[i] == k) ret++;
}
}
return ret;
}
}
- 时间复杂度:O(N^2)
- 空间复杂度:O(N)
思路2:思路1空间优化(O(N^2),O(1))
1、思路1使用了一个额外数组来存放前缀和,我们可以直接把前缀和存放在nums数组上,将空间复杂度降为O(1),但时间复杂度不变。
前缀和的计算公式和思路1相比需要做些修改:
nums[0~i]的和存放在nums[i]上,所以nums[i]=nums[i-1]+nums[i],i从1开始。
2、nums数组处理完时,nums数组保存的就是一个个前缀和,和思路1相同,在nums上双重遍历,统计数组中和为k的连续子数组个数:
- 如果i>0,则原数组nums[i~j]的和=现数组nums[j]-nums[i-1];
- 如果i==0,则原数组nums[i~j]的和=现数组nums[j]。
实现代码:
class Solution {
public int subarraySum(int[] nums, int k) {
int len = nums.length;
//构造前缀和数组
for(int i = 1; i < len; i++){
nums[i] = nums[i - 1] + nums[i];
}
int ret = 0;
for(int i = 0; i < len; i++){
for(int j = i; j < len; j++){
if(i == 0){
if(nums[j] == k) ret++;
}
else if(nums[j] - nums[i - 1] == k) ret++;
}
}
return ret;
}
}
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
思路3:前缀和+哈希表(O(N),O(N),推荐)
1、遍历数组nums,计算前缀和sum,即sum=nums[0i]的和(其中i=0n-1),每一个前缀和都保存到哈希表中:
- 如果第一次遇到一个前缀和值,则value置1;
- 如果是重复的前缀和,则对应的value+1。
2、设置一个ret保存满足条件的连续子数组的数量,在遍历nums的同时统计ret:
对于每一个计算得到的前缀和sum,都做如下判断:
- 判断sum-k是否存在于哈希表中,如果sum-k存在于哈希表中,说明从当前下标i处往前存在一个或多个和为k的连续子数组,ret加上sum-k对应的value,这里的value表示nums数组中连续子数组和为k的数量;
- 判断sum是否等于k,如果等于k说明找到一个满足条件的子数组,ret+1,并将这个sum存入map中:
- 如果sum存在于map中,则map中对应的value+1;
- 如果sum不存在于map中,则直接插入,value置1.
实现代码:
class Solution {
public int subarraySum(int[] nums, int k) {
int sum = 0;//存放计算得到的前缀和
int ret = 0;//统计满足条件的子数组个数作为最终结果
//map的key=前缀和,value=出现的次数
Map<Integer, Integer> map = new HashMap<>();
for(int i = 0; i < nums.length; i++){
sum += nums[i];
//如果sum-k存在于map中,说明在i之前存在一个或多个和为k的连续子数组,ret加上sum-k对应的value
if(map.containsKey(sum - k)) ret += map.get(sum - k);
//如果sum等于k说明找到一个满足条件的子数组,ret+1
if(sum == k) ret++;
//将每个前缀和都加入map中
int val = map.getOrDefault(sum, 0);
map.put(sum, val + 1);
}
return ret;
}
}
- 时间复杂度:O(N)
- 空间复杂度:O(N)