一、问题描述
给定一个二元数据nums,和一个整数goal,请你统计并返回有多少和为goal的非空子数组。其中子数组是数组中的一段连续部分。
示例1:
输入:nums = [1,0,1,0,1], goal = 2
输出:4
解释:有 4 个满足题目要求的子数组:[1,0,1]、[1,0,1,0]、[0,1,0,1]、[1,0,1]
提示:
- 1 <= nums.length <= 3 * 104
- nums[i] 不是 0 就是 1
- 0 <= goal <= nums.length
二、解题方法
2.1 暴力双循环
看到这个题最先想到的也是最直接最简单的就是双for暴力破解了。解题思路如下:
- 下标i从零开始遍历数组,用一个新的变量num记录当前下标的数组值,若num==goal,则计数器加一;
- 否则,创建新的下标j,令num+=nums[j],若num==goal,计数器加一,大于goal则跳出循环。这是因为题目给出了限定条件,数组中元素都为0,1。且子数组需要是连续的数组,所以遇到不相等的情况可以直接跳出循环。
- i++,从下一个元素开始重新执行下一个内部循环。
代码如下:
/**
* @param {number[]} nums
* @param {number} goal
* @return {number}
*/
var numSubarraysWithSum = function(nums, goal) {
let num=0,count=0;
for(let i =0;i<nums.length;i++){
num=nums[i];
if(num==goal){
count++;
}
for(let j=i+1;j<nums.length;j++){
num+=nums[j];
if(num==goal){count++;}
else if(num>goal) {break;}
}
}
return count;
}
2.1的双for循环在时间复杂度为O(n^2)上有点不尽人意,所以这里给出两个官方给的解法。
2.2 哈希表+前缀和
具体思路如下:
**前缀和:**假设preSum[i+1]为数组前i项的和,则nums[i]到nums[j]的连续序列和sum[i,j]为:preSum[j+1]-preSum[i];
**哈希表:**所求解子序列之和goal可通过前缀和获得:goal=sum[i,j]=preSum[j+1]-preSum[i]。数组中任意两个数之和等于给定目标数,思路可参考两数之和,使用哈希表,时间复杂度为O(n)。
计数:对于每个preSum[j+i],与其关联的解个数等于preSum[i]的个数,计数方式为:count+=count(preSum[i])。其中,i<j,count(preSum[i])为preSum[i]在前缀和数组前j项中出现的个数。
var numSubarraysWithSum = function (nums, goal) {
const preSum=[0];
for(const num of nums)
//将前i项的和压入栈
preSum.push(preSum[preSum.lenght-1]+num);
let count=0;
//这里添加了(0,1)这一项是因为之后要是满足了sum-goal=0,就说明原数组中存在sum=goal的这个子数组,需要计数加一。所以手动写入了。
const preSumCount=new Map().set(0,1)
for(let i=0;i<nums.length;++i){
// goal = preSum[i + 1] - preSum[prev] -> preSum[prev] = preSum[i + 1] - goal
count+=(preSumCount.get(preSum[i+1]-goal)||0);
preSumCount.set(preSum[i+1],(preSumCount.get(preSum[i+1])||0)+1);
//key为和的值,value为这样的和的子数组个数
}
return count;
};
简化代码:
var numSubarraysWithSum =function(nums,goal){
let sum=0;
const sumcount=new Map();
let count=0;
for(const num of nums){
sumcount.set(sum,(sumcount.get(sum)||0)+1);
sum+=num;
count+=sumcount.get(sum-goal)||0;
}
return count;
}
2.3滑动窗口
具体思路:这个思路其实是在上面的思路转化过来的,对于方法2.2中的每一个j,满足sum[j]-sum[i]=goal的i总是落在一个连续的区间中,i值取区间中每一个数都满足条件(这里是因为数组中的元素均为0,1)。并且伴随j右移,其对应的区间的左右端点也将右移,可以使用滑动窗口解决本题。
具体地,我们令滑动窗口右边界为 right,使用两个左边界 left1和 left2表示左区间 [left1,left2),此时有 left2−left1 个区间满足条件。
在实际代码中,我们需要注意 left1≤left2≤right+1,因此需要在代码中限制 left1和 left2不超出范围。
var numSubarraysWithSum =function(nums,goal){
const n=nums.length;
let left1=0,left2=0,right=0;
let sum1=0,sum2=0;
let ret=0;
while(right<n){
sum1+=nums[right];
while(left1<=right&&sum1>goal){
sum1-=nums[left1];
left1++;
}
sum2+=nums[right];
while(left2<=rigth&&sum2>=goal){ //这一句sum2>=goal就很精妙
sum2-=nums[left2];
left2++;
}
ret+=left2-left1;
right++;
}
return ret;
};
这个思路我和同门还讨论了一会,实现的时候写了一大串复杂的代码,从思考到转变为代码再到简化代码,真的道阻且长。