1. 什么是前缀和?
前缀和是指一个数组的某项下标之前(包括此项元素)的所有数组元素的和。其实就类似我们中学时候学过的数列的前n项和。
数列前n项和公式:假设有数列
其前n项和公式:
对应到现在的数组中,就是假设有长度为n的数组
那么他的前缀和数组公式就是:
为了计算的方便我们定义sum数组的长度为n+1,上述公式中不涉及的sum[0],我们将其值设置为0(即)。
2. 获得前缀和数组后我们便可以快速计算原数组某区间的各元素之和了。前缀和数组有如下性质:
其中,interval[i, j]表示nums数组区间i到j之间的元素和。
3. 练手题目:
class NumArray {
public int[] nums;
public NumArray(int[] nums) {
this.nums = nums;
}
public int sumRange(int left, int right) {
int[] sum = new int[nums.length + 1];
sum[0] = 0;
for(int i = 1; i <= nums.length; i++) {
sum[i] = sum[i - 1] + nums[i - 1];
}
return sum[right + 1] - sum[left];
}
}
这道题是计算数组所有奇数长度子数组的和,涉及到子数组,可以用前缀和来做。当然,这道题有效率和空间更优的解法,这里只给出前缀和的方式。(数学方法之后会再写一篇文章介绍)
public int sumOddLengthSubarrays(int[] arr) {
int len = arr.length;
int[] sum = new int[len + 1];
sum[0] = 0;
for(int i = 1; i <= len; i++) {
sum[i] = sum[i - 1] + arr[i - 1];
}
int res = sum[len];
if(len < 3) {
return res;
}
for(int i = 3; i <= len; i += 2) {
res += subSum(sum, i);
}
return res;
}
public static int subSum(int[] sum, int gap){
int res = 0;
for(int i = 0; i + gap < sum.length; i++) {
res += (sum[i + gap] - sum[i]);
}
return res;
}
一看题目又涉及到子数组了,考虑用前缀和。
首先,这道题用暴力是可以解的,就是双层for循环,但是这么做的话,时间复杂度就是O(n^2),所以就需要考虑用hashmap+前缀和的方式了。
首先,我们知道前缀和就是用来解决子数组和问题的,那么根据题意我们要找到和为k的子数组,也就是要找到区间在间的子数组,其和为k。
那么,其实我们也就是要找到:
也就是:
意思就是说,我们在向后遍历前缀和数组的时候,如果当前位置的前缀和减去k的值,在已经遍历过的地方出现过,那么我们就找到了sum[i],可以使上式成立,也就找到了满足条件的子数组。
举个例子:nums = [1, 2, 3, 4, -3, -1, 4, 3] k = 3
当我们遍历到6的时候,发现6-3=3,而在遍历过的前缀和数组中出现了3,那么我们就找到了一个满足条件的区间。
但是,这个3有可能出现多次,而它出现几次我们就相当于找到了几次,然后,想精准快速地找到数值的时候,我们还想用到contains方法,因此很容易联想到用hashmap。
于是,就可得到如下题解:
public int subarraySum(int[] nums, int k) {
int sum = 0;
int count = 0;
Map<Integer, Integer> map = new HashMap<>();
map.put(0, 1);
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
if (map.containsKey(sum - k)) {
count += map.get(sum - k);
}
map.put(sum, map.getOrDefault(sum, 0) + 1);
}
return count;
}
这道题,最好的解法也不是前缀和,而是滑动窗口。但是可以采用前缀和的方法来做。
先看代码:
public int minSubArrayLen(int target, int[] nums) {
int len = nums.length;
int[] sum = new int[len + 1];
sum[0] = 0;
for (int i = 1; i < len + 1; i++) {
sum[i] = sum[i - 1] + nums[i - 1];
}
int res = Integer.MAX_VALUE;
for (int i = 0; i < len; i++) {
int sumj = target + sum[i];
int index = Arrays.binarySearch(sum, sumj);
if (index < 0) {
index = -index - 1;
}
if (index <= len) {
res = Math.min(res, index - i);
}
}
return res == Integer.MAX_VALUE ? 0 : res;
}
首先这道题用到的原理任然是:
只不过,这道题里面要找的是:
所以,在代码中我们先求了k+sum[i] (代码中k就是target)
然后,这次我们不去一个一个寻找了,可以直接用二分查找法去找大于等于k的值的最小下标。
这里说一下,为什么有这段代码:
if (index < 0) {
index = -index - 1;
}
if (index <= len) {
res = Math.min(res, index - i);
}
原因是Arrays.binarySearch(sum, sumj)这个方法如果找不到,则返回 (-(插入点)-1),所以需要在这里处理一下。大家可以仔细阅读一下这个方法的源码。
最后,还需要注意的一点是:后面这个for循环里面的 i 是按照 nums[i] 数组走的。当然,你也可以按照sum数组遍历,leetcode官方答案就是按照sum走的,都可以,但是不要搞混了。