Offer 驾到,掘友接招!我正在参与2022春招打卡活动,点击查看活动详情。
给你一个整数数组 nums 和一个整数 k ,编写一个函数来判断该数组是否含有同时满足下述条件的连续子数组:子数组大小 至少为 2 ,且子数组元素总和为 k 的倍数。如果存在,返回 true ;否则,返回 false 。
如果存在一个整数 n ,令整数 x 符合 x = n * k ,则称 x 是 k 的一个倍数。0 始终视为 k 的一个倍数。
此题要求解的是连续子数组和,看到连续子数组,最简单的办法就是暴力穷举,遍历每一个子数组的和是否是k的倍数,代码如下:
public static boolean checkSubarraySum2(int[] nums, int k) {
if (nums.length < 2) {
return false;
}
List<Integer> list = new ArrayList<>();
for (int i = 0; i < nums.length; ++i) {
for (int j = i; j < nums.length; ++j) {
list.add(nums[j]);
if (getSum(list) % k == 0) {
if(list.size() < 2){
continue;
}
return true;
}
}
list.clear();
}
return false;
}
public static int getSum(List<Integer> list) {
return list.stream().reduce(Integer::sum).get();
}
需要注意0的特殊情况,例如:[5,0,0,0],对于这个数组,其子数组[0,0]元素和为0,而题目中明确说明了0始终视为k的一个倍数;又如:[1,0],虽然其子数组[0]也是k的一个倍数,但它不满足子数组大小至少为2的条件,所以在代码中需要处理一下元素0的特殊情况,只有当子数组中有2个及以上的元素0时才是满足要求的。
不过暴力穷举的时间复杂度很差,甚至都无法通过:
所以,我们应该考虑另外一种解法,前缀和。
举个例子,对于数组[23,2,4,6,7],我们可以求出元素之间的和,如下:
[23+0, 23+2, 23+2+4, 23+2+4+6, 23+2+4+6+7] = [0, 23, 25, 29, 35, 42]
求出这么一个数组有什么作用呢?当我们想知道某个子数组的和时可以很方便地求得,例如想求子数组[2, 4, 6],我们无需遍历到子数组元素再相加,只需要让元素6位置的前缀和减去元素2位置的前缀和即可,35 - 23 = 12。
继续来看,若是想求解子数组[2,4,6]是否为6的倍数,我们可以将整个问题转换为6位置的前缀和减去2位置的前缀和之差取模6是否为0,若是等于0,则其满足同余定理。
同余定义的解释如下:
给定一个正整数m,如果两个整数a和b满足a-b能够被m整除,即(a-b)/m得到一个整数,那么就称整数a与b对模m同余,记作a≡b(mod m)。对模m同余是整数的一个等价关系。
我们暂且设6位置的前缀和为pre1,2位置的前缀和为pre2,那么则有:
- 当(pre1 - pre2) % m == 0时,pre1 % m == pre2 % m
反过来也是等价的,所以我们只需要求得两个前缀和分别取模k是否相等即可得到对应的子数组和是否是k的倍数,代码如下:
public static boolean checkSubarraySum(int[] nums, int k) {
int n = nums.length;
// 计算前缀和
int[] prefixNum = new int[n + 1];
for (int i = 1; i <= n; i++) {
prefixNum[i] = prefixNum[i - 1] + nums[i - 1];
}
// Set集合用于保存取模的结果
Set<Integer> set = new HashSet<>();
for (int i = 2; i <= n; i++) {
// 保存取模结果
set.add(prefixNum[i - 2] % k);
// 若是当前位置前缀和取模k与set集合中的某个前缀和取模k的结果相同
// 则一定有对应的子数组和为k的倍数
if (set.contains(prefixNum[i] % k)) {
return true;
}
}
return false;
}