解法二:前缀和加哈希表
计算 [0, i] 区间的前缀和 sum[i]。
我们把要求的 K 子数组定义为以 i 结尾的子数组。如果存在 K 的子数组的话,如果 i 数组前面出现过子数组和为 sum[j] 的话,就说明存在 K 数组,sum[j] = sum[i] - K。
我们在计算前缀和 sum[i] 的时候,其实也就已经得知 i 数组前面所有的下标的前缀和,那么我们可以利用哈希表来保存这些数据,哈希表存储的应该是 <sum[i], 出现的次数>。
我们其实不需要创建前缀和数组,因为我们只是要利用前一个前缀和结果来推导现在这一个前缀和,对于前几次的前缀和结果我们其实用不到,所以我们可以使用一个变量 sum,一直滚动累加下去即可。
要事先将 <0, 1> 存进哈希表中,因为可能会出现一开始就得到和为 K 的数组,此时 sum - k = 0,但是 count 却没有自增,所以我们事先设定存在一个前缀和为 0 的子数组,并且出现次数为 1。
classSolution {
publicintsubarraySum(int[] nums, int k) {
Map<Integer, Integer> map = newHashMap<>();
map.put(0, 1);
intsum=0;
intcount=0;
for (int x : nums) {
sum += x;
if (map.containsKey(sum - k)) {
count += map.get(sum - k);
}
map.put(sum, map.getOrDefault(sum, 0) + 1);
}
return count;
}
}
和可被 K 整除的子数组
解析:这里依旧不能使用滑动窗口来做,因为存在负数,不具有单调性。
补充知识:同余定理,当 (a - b) % p = 0 可以推导出 a % p = b % p。有了这个公式,我们就可以得出当存在另外几个 b 满足 a % p = b % p 的时候,则说明存在和可被 K 整除的子数组。在判断的时候,我们用到的是余数,所以在哈希表中,我们保存的应该为 <余数,余数出现的次数>,后面就是基本的前缀和操作了。
Java 当中,负数 % 正数的结果为负数,我们需要将结果纠正为正数,公式为 ret = (sum % k + k) % k。
解释一下,sum % k 可以得到余数,但是可能为负数,所以再加一个 k,保证这是一个正数,但是可能会破坏余数这个特性,所以还要再次模 k,最后的模 k 是不会影响的。
由于可能会出现一开始前缀和正好为 K,取模结果为 0,此时应该预先将 <0, 1> 存储进哈希表里。
classSolution {
publicintsubarraysDivByK(int[] nums, int k) {
Map<Integer, Integer> map = newHashMap<>();
map.put(0, 1);
intsum=0;
intcount=0;
for (int x : nums) {
sum += x;
intret= (sum % k + k) % k;
count += map.getOrDefault(ret, 0);
map.put(ret, map.getOrDefault(ret, 0) + 1);
}
return count;
}
}
连续数组
解析:由于数组只会出现 0 或者 1,我们要寻找最大长度的数组(满足 0 的数量等于 1 的数量),如果要使用前缀和,我们可以将 0 设置为 -1,这样,我们要寻找的子数组(记为 K)的和就是为 0。
前缀和数组 sum[i]:此时 K = 0,那么就是要得到目前是否存在 sum[j] 数组是等于 sum[i],如果存在,则需要计算这个 K 数组的区间长度 = i - j。
于是我们可以将前缀和与对应的下标索引存到哈希表里,<sum,下标>。
我们不用真的创建一个前缀和数组,因为我们用不到,我们可以使用一个变量 sum 来保存当前的前缀和结果。
如果当整个数组的和为 K 的时候,那么最大的长度应该为整个数组的长度,此时 i 最大到达 nums.length - 1,要想获得数组长度,我们应该预先将 <0, -1> 存储进哈希表中。
哈希表的 put:我们保存数据的时候,要保存的是 sum 对应的最小的索引,因为我们要求的是最大的数组长度,所以当哈希表存在 sum[i] 的时候,我们应该更新最新长度,这个时候,就不用保存进哈希表里了,因为我们不需要跟新下标索引值,如果没有出现的话,那么此时 sum[i] 所对应的下标要一起保存进哈希表中。