题目:
给定一个整数数组和一个整数 k ,请找到该数组中和为 k 的连续子数组的个数。
示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2
解释: 此题 [1,1] 与 [1,1] 为两种不同的情况
示例 2 :
输入:nums = [1,2,3], k = 3
输出: 2
提示:
1 <= nums.length <= 2 * 10^4
-1000 <= nums[i] <= 1000
-10^7 <= k <= 10^7
分析:
拆解关键词:
【整数数组、正整数、负整数、整数K、连续子数组、求和】
想法:
🤔做了很多这种子数组的题目,相信大家和我一样,看了这种题,可能立马会浮现三种解决方式,那我们来一一分析是否可以继续使用如下三种方式:
1、暴力破解法
2、前缀和
3、滑动窗口法
解释:
【暴力破解法】
假设给定一个数组**【2,3,1,2】**,该数组存在的全部连续子数组的情况有如下:数组长度最小是1,最大是本身的长度。
⚠️分数组长度:
数组长度为1: 【2】【3】【1】【2】
数组长度为2: 【2,3】【3,1】【1,2】
数组长度为3:【2,3,1】【3,1,2】
数组长度为4:【2,3,1,2】
上面这种很好理解吧,就是分大小然后单独列出来,我这里将上面的数据换种方式展示【注意是一模一样的数据,只是换了分类方式】
⚠️分数组起始位置:index是下标位置 这里index的范围是0到3
index=0: 【2】【2,3】【2,3,1】【2,3,1,2】
index=1: 【3】【3,1】【3,1,2】
iddex=2: 【1】【1,2】
index=3: 【2】
可以发现,从起始位置开始向后扩范围,可以将所有的可能解挨个循环,这样可以保证不漏掉任何一个连续子数组。
所以我在描述暴力破解的时候,只需要每次从一个下标循环,然后在这个下标位置下二次循环,向后每次扩大一位数组,是可以得出全部的解。
【前缀和】
之前涉及到的前缀和是和二分查找一起使用的,这也就给了一个错觉,好像只有前缀和是递增或者递减的情况下才可以使用,但是错了,我错了,我不应该想的这么片面。这道题其实也可以使用前缀和。这里我会详细的描述前缀和的解法。
这里假设题目给了一个较为复杂且正负交叉存在的数组:【0,1,2,-2,1,-1,3,-3,6,10,-16】很复杂对吧,下面我们列举前缀和:
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
nums[i] | 0 | 1 | 2 | -2 | 1 | -1 | 3 | -3 | 6 | 10 | -16 |
sums[i]-前缀和 | 0 | 1 | 3 | 1 | 2 | 1 | 4 | 1 | 7 | 17 | 1 |
上述数组sums[i]
就从下标0
为起始点的nums[0->i]
的和。⚠️假设题目给的k=0
,那么按照sums
的结果来看,只有sums[0]
这一个结果满足k
,但是其实从肉眼来看,数组中有很多连续的和满足0
的元素,比如【2,-2】【3,-3】等等,这些都是结果,但是从sums
数组上却看不出来
🤔原因是什么呢?
原因就是sums
是恒从下标0
开始的累加值,所以看不到其他的存在的解。
那怎么看到其他潜在的解呢,有一个思路,假设要寻找【2,-2】,那么其实从我们个人解题来讲,需要找的是sums[3]-sums[1]
的结果就是nums[2]+nums[3]
,加起来正好是符合和=0
.
⚠️下面这个结论很重要,大家一定要理解,理解了才可以写出最终的代码:
💡得:如果满足`sums[i]`-`sums[j]`=`k`,且`i>j`那么存在新的解,新的解就是`下标i`-->`j`的和。反过来说,如果`sums[i]`-`k`的结果在`sums`数组中存在,那么也存在新的解。这两句话意思一样,一个是正着说,一个反着说,如果不理解可以多看几遍😝
整体思路已经明了,接下来就是扣细节的任务了:
- 如果
sums[i]
-k
的结果在sums
数组中存在多个,比如我们题目所给的例子来看,sums[5]
-sums[3]
和sums[5]
-sums[1]
的结果是一样的,那么反推过来sums[5]
-k
肯定也是有两个解的【这里我们依旧假设k
是题目要求为0
】,这两个结果我们都得要。因为这就是代表多个起点不一致的连续满足解的子数组,当然都要了哈。
- 那这里如何体现有几个解呢,可以使用
map
结构,来存储,以sums[i]
-k
的值为key
,以sums[i]
-k
出现的个数为value
,这样就可以做到体现有几个解了,而map
结构中的key
其实就是当前出现过的sums[j]
的值(j<i
)
【滑动窗口法】
考虑下这里是否可以使用滑动窗口,滑动窗口有左右指针构成,当不满足条件的时候,right
指针右移,当满足满足条件且right
继续右移已经不可能出现答案的时候那么右移left
指针。企图通过移动left
指针去发现新的解。
这里什么时候可以满足移动right
指针不可能再出现答案呢? 不存在这个时候。假设当前是寻找sum=k
,因为数据中存在正数和负数,所以窗口的right
指针永远都需要继续移动,因为移动后sum
可能变大也可能变大,可能寻找到新的解,直接遍历到元素末尾才能确定这一轮解最后的结果。
这种目标无法明确,窗口指针左右边界几乎是整个数组范围,这种情况下确实是无法使用滑动窗口,这个机制下使用滑动窗口无异于暴力破解。故放弃该解法。
代码:
第一版:暴力破解
class Solution {
public int subarraySum(int[] nums, int k) {
return first01(nums,k);
}
public static int first01(int[] nums,int k){
int len = nums.length;
int res_cnt = 0;
for(int i=0;i<len;i++){
int sum = nums[i];
if(sum==k) res_cnt++;//判断是否存在一个元素直接等于k
for(int j=i+1;j<len;j++){
sum+=nums[j];//累加
if(sum==k) res_cnt++; //如果累加结果等于k,那么更新结果解的个数
}
}
return res_cnt;
}
}
第二版:前缀和
class Solution {
public int subarraySum(int[] nums, int k) {
return first02(nums,k);
}
public static int first02(int[] nums,int k){
int len = nums.length;
int sum = 0;
int res_cnt = 0;
HashMap<Integer,Integer> map = new HashMap<>();
for(int i=0;i<len;i++){
sum = sum + nums[i]; //计算前缀和
int tmp = sum - k; //获取tmp值,如果当前存在满足tmp的值,那么说明sums[i]至少一个存在一个下标j,使得sums[j]--》sums[i]满足连续子数组和=k
if(tmp==0)res_cnt++;//如果tmp等于0,那么说明sums[i]本身是一个解,不需要去寻找sums[j].可以直接将结果更新
res_cnt = res_cnt + map.getOrDefault(tmp,0); //加上tmp存在的个数 如果不存在那么+0
map.put(sum,map.getOrDefault(sum,0)+1);//更新新的sum值,便于后续继续查找使用
}
return res_cnt;
}
}
总结:
这道题给我最大的帮助,就是让我对前缀法的应用又多了一层了解,前缀法虽然计算每个元素从开始到截止位置前缀很简单,但是计算好之后我们如何使用这个和去实现题目要求才是最重要的,所以算法确实变化太多了,需要我们不断的去提高,去适应,去解决。💪🏻💪🏻💪🏻
大家好,我是二十一画,感谢您的品读,如有帮助,不胜荣幸~😊