我们刷leetcod时总是遇到各种各样的子数组和的问题,这类问题一般都是可以通过构建一个前缀和数组,以O(N^2)的时间复杂度求解,但这并不是最优解。
目录
问题一:和为K的数组
问题描述:
给定一整型数组nums和一整数k,找到该数组中和为k的子数组的个数。
input : nums = [1,2,3,-3,-2,-1,0] k = 0
output: 5 分别为[3,-3], [2,3,-3,-2], [1,2,3,-3,-2,-1], [1,2,3,-3,-2,-1,0] , [0]
解法一:
依次以每个元素作为头结点,以其之后元素作为尾结点,判断此时的子数组和是否为k。由于判断该子数组的和的复杂度可以使用前缀和数组由O(N)降到O(1),实现代码如下:
public int subarraySum(int[] nums, int k) {
int[] sums = new int[nums.length + 1];
for(int i = 0; i < nums.length; i++){
sums[i + 1] = sums[i] + nums[i];
}
int result = 0;
for(int start = 1; start < sums.length; start++){
for(int end = start; end < sums.length; end++){
if(sums[end] - sums[start - 1] == k){
result++;
}
}
}
return result;
}
tip:计算前缀和数组时,可以多给一位,sums[i] 为从开头到i-1位置之和,这样做的好处是后面在处理以第一个节点开头的子数组时不用特殊处理。
解法二:
对于前缀和数组还可以进行如下优化,使用一HashMap结构,key为遍历过程中的前缀和,value为当前前缀和出现的次数,因此遍历过程中便可以在map中寻找(sum - k)的值是否出现过,若存在则证明以当前结点结尾有map.get(sum - k)个子数组。时间复杂度降为O(N)。
public int subarraySum(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();// k = sum ,value = 次数
int sum = 0;
int result = 0;
map.put(0, 1);
for(int num : nums){
sum += num;
if(map.containsKey(sum - k)){
result += map.get(sum - k);
}
map.put(sum, map.containsKey(sum) ? map.get(sum) + 1 : 1);
}
return result;
}
问题二:和等于k的最长/短子数组
问题描述:
给定一整型数组nums及一整数k,找到nums中最长/短的和等于k的子数组,不存在返回-1。本题求解的是最长的子数组,最短的同理。
input : nums = [1,2,3,-3,-2,-1,0] k = 0
output:7 而不能是这些[3,-3], [2,3,-3,-2], [1,2,3,-3,-2,-1], [0]
解决方案:
如之前介绍,该问题依然可以使用前缀和数组,将所有满足条件的子数组都求解出来,返回其中最大的。如此还是O(N^2)的时间复杂度,该解法就不加赘述了。
遍历过程找到当前结点为尾结点的满足条件的最长子数组。
该问题依然可以使用一HashMap来优化,key存储的是遍历过程中的前缀和,不同的是value存储的是该前缀和最早出现的位置。由于找的是最长的,由于后面再次出现该sum时,得到满足条件的子数组总是比前面的更短。实现代码如下:
public static int maxSubArraySum(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
map.put(0, -1);
int sum = 0;
int result = -1;
for(int i = 0; i < nums.length; i++) {
sum += nums[i];
if(map.containsKey(sum - k)) {
result = Math.max(result,
i - map.get(sum - k)); // 从map.get(sum - k) + 1 到 i为子数组
}else {
map.put(sum, i);
}
}
return result;
}
问题三:和为k的整数倍的数组
题目描述:
给定一非负的整型数组,以及一整数k。判断该数组中是否存在一子数组使得组数组的和等于k的整数倍。要求子数组长度大于2。
input : nums = [23,2,4,6,7], k = 6
output : true
解决方案:
该问题的一般解法还是可以通过构建前缀和数组遍历出所有的子数组,以O(N^2)的时间复杂度求解,不过需要注意的是,该问题中要求k的整数倍,因此不可避免的需要对k取余,因此对于k=0的情况需要特殊处理.。
想要使用HashMap优化,需要确定key的含义。
sum1 % k == sum2 % k => (sum2 - sum1) % k == 0
不妨设 sum1 = k * m + p, sum2 = k * n + p,则(sum2 - sum1) % k =(n - m) * k % k ==0
因此可以使用当前前缀和对k取余的余数作为key,遍历过程,若出现当前前缀和的余数之前出现过,则说明中间这一段对k取余等于0。由于题目还要求子数组的长度至少为2,因此将当前的下标作为value。
此外上述所提的下标应该为该余数第一次出现的下标。例如案例[ 1 7 7] k = 7就可以说明问题。实现代码如下:
class Solution {
public boolean checkSubarraySum(int[] nums, int k) {
if(k == 0){
return checkZeroOption(nums);
}
Map<Integer, Integer> map = new HashMap<>();
map.put(0, -1);
int sum = 0;
for(int i = 0; i < nums.length; i++){
sum += nums[i];
if(map.containsKey(sum % k)){
if(i - map.get(sum % k) > 1){
return true;
}
}else{
map.put(sum % k, i);
}
}
return false;
}
// 处理 k=0情况
public boolean checkZeroOption(int[] nums){
for(int i = 1; i < nums.length; i++){
if(nums[i - 1] == 0 && nums[i] == 0){
return true;
}
}
return false;
}
}