我们将得到一个整数数组和一个值k
。我们需要找到总和等于k
的子数组的总数。
我在网上找到了一些有趣的代码(在Leetcode上),如下所示:
public class Solution {
public int subarraySum(int[] nums, int k) {
int sum = 0, result = 0;
Map<Integer, Integer> preSum = new HashMap<>();
preSum.put(0, 1);
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
if (preSum.containsKey(sum - k)) {
result += preSum.get(sum - k);
}
preSum.put(sum, preSum.getOrDefault(sum, 0) + 1);
}
return result;
}
}
为了理解这一点,我浏览了一些具体示例,例如[1,1,1,1,1]
k=3
和[1,2,3,0,3,2,6]
k=6
。虽然代码在两种情况下都能很好地工作,但我无法理解它实际上如何计算输出。
1)为什么代码会连续添加数组中的值,而不会将其归零?例如,如果[1,1,1,1,1]
加k=3
,sum=3
一次,我们是否需要将sum
重置为零?不会重置sum
干扰查找以后的子阵列吗?
2)当我们找到总和result++
的子数组时,我们不应该只是k
吗?为什么我们要添加preSum.get(sum-k)
?
答案 0 :(得分:11)
代码对数组进行求和并且不重置sum
的原因是因为我们在preSum
(之前的总和)中保存总和。然后,只要我们到达sum-k
之前的总和(比如索引i
),我们就知道之间的总和索引i
和我们当前的索引正是k
。
例如,在下面带有i=2
的图片中,我们当前的索引等于4
,我们可以看到自9
以来,我们当前索引的总和减去{{ 1}},索引3
的总和为i
,索引6
和2
(包括)之间的总和为4
。
考虑这一点的另一种方法是看到从数组中丢弃6
(在我们当前的[1,2]
索引处)给出了一个总和4
的子数组,原因类似于以上(详见图片)。
使用这种思维方法,我们可以说我们想要从数组的前面丢弃,直到我们留下一个sum 6
的子数组。我们可以这样说,对于每个索引,“丢弃k
,然后丢弃1
,然后丢弃1+2
等”(这些数字来自我们的示例),直到我们找到sum 1+2+3
的子数组(在我们的示例中为k
)。
这提供了一个非常有效的解决方案,但请注意我们将在数组的每个索引处执行此操作,从而反复对相同的数字求和。保存计算的一种方法是保存这些总和以供以后使用。更好的是,我们已经将这些相同的数字加在一起得到我们当前的k=6
,所以我们可以保存这个数据。
要查找子阵列,我们只需查看已保存的总和,减去它们并测试我们剩下的是sum
。必须减去每个已保存的总和有点烦人,所以我们可以使用commutativity of subtraction来查看如果k
为真,sum-x=k
也是如此。通过这种方式,我们可以看到sum-k=x
是否是已保存的总和,如果是,则知道我们找到了大小为x
的子数组。哈希映射使这种查找更有效。
大多数时候你是对的,在找到合适的子阵列后我们可以做k
。几乎总是,result++
中的值将为preSum
,因此1
将等同于result+=preSum.get(sum-k)
或result+=1
。
唯一不合适的时间是result++
之前已经达到的preSum.put
。我们怎样才能回到我们已经拥有的sum
?唯一的方法是使用负数来取消之前的数字,或者使用零,这根本不会影响总和。
基本上,当子阵列的总和等于0时,我们回到先前的sum
。这类子阵列的两个例子是sum
或普通的[2,-2]
。使用这样的子阵列,我们需要将[0]
添加到1
result
或k+2-2=k
,因此我们找到了多个子阵列,其中一个为零sum子阵列(k+0=k
)和没有它的一个(sum=k+0
)。
这也是sum=k
中+1
的原因。每当我们再次达到相同的preSum.put
时,我们就会找到另一个零和数组。使用两个零和子阵列,我们有三个带有sum
的子阵列:原始数组(sum=k
),原始数据加上第一个(sum=k
)和原始数据(sum=k+0
)({{ 1}})。这种逻辑也适用于更多数量的零和子阵列。