560. 和为K的子数组
给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。
示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
来源:力扣(LeetCode)
先说说简单的思路:
最简单的当然是两个for遍历数组,让每一个下标都作为起点,然后从起点遍历到结尾,看看中间有多少个连续的子数组和为k,这里我就随便写写核心部分
int sum = 0,res = 0
for(int L = 0;L<nums.size();++L)
{
sum = 0;
for(int R = L;R<nums.size();++R)
{
sum += nums[R];
if(sum = k)
++res;
}
}
return res;
很明显,这种方法的时间复杂度为O(N^2) , N为数组长度
效率相当低
接下来我们介绍一种前缀和数组,通过这个数组我们可以随时通过O(1)的时间复杂度找到任何子数组的和,你可能会问,这数组对这题的优化好像没啥帮助啊?
确实没有直接用到前缀和数组,但是用到了这个思路
前缀和数组:
优化方法:
利用前缀和数组的思路的方法就是:
用哈希表记录每种前缀和出现的次数
具体思路:
只需一次遍历数组,遍历的时候记录每一种前缀和出现的次数,然后用一个变量sum记录数组开头到当前位置的总和,
暴力法里我们是找L和R,只有当前sum[R+1]-sum[L]==k时才计数+1,这种方法需要两层循环,一个改变L,一个改变R,
于是我们反过来看,遍历到的位置就是R,然后我们再从前面找有几个符合条件的L使得sum[L] = sum[R]-k,因为L已经遍历过了,所以用哈希表记录之后,可以用O(1)的时间寻找,非常典型的空间换时间
因为数组中会出现负数,所以可能会有重复的前缀和,因此一边遍历数组一边构建哈希表最好,不然在当前位置的后面出现了个重复的,这样差哈希表的时候,计数就会不正确,子数组,肯定是从前面开始,后面的肯定不算数
哈希表的键就是前缀和,值是前缀和出现的次数
这里要注意一下一个特殊的前缀和:0,和前缀和数组一样,这是个必须的初值
举个例子
nums = [1,2,3,-2,-1] , k = 3
我们遍历到2得时候,前缀和1、3都被填入哈希表了,当然1 != 3我们跳过,然后到了3,很明显满足条件,但是,在程序判断时,程序会认为不满足条件,为什么?
因为我们没有0啊,我们要找的是sum[L]的出现次数,此时sum[R]正好等于3,所以sum[L]应该为0,但哈希表却没有0的键,很明显就出问题了嘛
代码+注释:
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> map;
map[0] = 1;//初值
int sum = 0,res = 0;
for(auto&temp:nums)//遍历数组
{
sum+=temp;//记录总和
int preSum = sum-k;//这个是我们要找的目标前缀和,即sum[L]
if(map.find(preSum)!=map.end())//如果找到了,就将它出现的次数加到结果上,
{
res += map[preSum];
}
++map[sum];//将当前的位置的前缀和填入数组
}
return res;
}
};
523. 连续的子数组和
给你一个整数数组 nums 和一个整数 k ,编写一个函数来判断该数组是否含有同时满足下述条件的连续子数组:
子数组大小 至少为 2 ,且
子数组元素总和为 k 的倍数。
如果存在,返回 true ;否则,返回 false 。
如果存在一个整数 n ,令整数 x 符合 x = n * k ,则称 x 是 k 的一个倍数。
来源:力扣(LeetCode)
这一题也是前缀和+哈希表的应用,但是和上一题不同的是,求的不是固定和,而是和必须为k的倍数,而且又要求长度最小为2
先说说k的倍数子数组应该怎么求,一开始我看见这题的时候就想到了前缀和,但是要求和是k的倍数,这一步就卡死我了,导致我写不出来,只能暴力搜索,当然最后超时了,看了题解就知道了有个叫 【 同余定理 】的东西,简单来说,就是如果两个数的差值是k的整数倍,那么这两个数除以k的余数相等
就是假设有两个数L, R 如果(R-L)%k == 0,那么R%k == L%k
这个定理就是关键,因为我们要用哈希表搜索,如果用求和再取余的方式的话,谁知道该搜索哪个数?于是只能暴力搜索,但是用同余定理的话,我们哈希表里存放(前缀和%k),就可以瞬间知道前面有没有符合的数
解决了第一个问题,接下来看看第二个问题,长度问题,很简单,哈希表里键存(前缀和%k),值里存下标不就完事了
最后说说特殊情况:
首先是最容易解决的:数组长度不足2,直接一个判断完事
然后是坑了我一次的:
数组的所有前缀和都一样,比如这种:
3,0,0,0,0,0,0,0
所有位置的前缀和都为3,这说明了什么,即不管哪个子数组(除了从3开始的),只要长度合格了,那么他就满足题目要求了,但是因为前缀和一直相等,我们一直在刷新哈希表里对应的下标,导致代码一直找不到长度为2的子数组
解决方法也很简单,不要一直刷新下标,只有哈希表里没有对应的键(前缀和%k)时,才记录下标
这样下标就会被固定在最初的下标
这也是与上一题不一样的地方,上一题要求和为k的数量,哈希表的值里存放的是出现次数,必须立马刷新,这一题存放的是下标,不能刷新
代码:
class Solution {
public:
bool checkSubarraySum(vector<int>& nums, int k) {
if(nums.size()<2)return false;
unordered_map<int,int>map;
map[0] = -1;//初值,判断从0开始的子数组是不是正好是k的倍数,从0开始的话,对应的L就是0嘛
//并且从-1开始,方便后续的判断和赋值,当然从0开始也完全没问题
int sum = 0;
for(int i = 0;i<nums.size();++i)
{
sum += nums[i];
//如果有在哈希表里存0的话,这一步可以不用写,作用一样的
//if((sum%k)==0 && i>=1)return true;
int temp = sum%k;//当前位置前缀和对应的余数
if(map.find(temp)!=map.end())
{
if(i-map[temp]>=2)//这里得注意一下,因为前缀和数组的特性,下标就是会少一个的
//所以这里不用再+1,或者>=1什么的,直接判断差值即可
return true;
}
else//找不到的时候再赋值,意思就是别刷新下标,乖乖的呆在初始位置吧
map[sum%k] = i;
}
return false;
}
};