题目
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
子数组是数组中元素的连续非空序列。
示例 1:
输入: nums = [1,1,1], k = 2
输出: 2
示例 2:
输入: nums = [1,2,3], k = 3
输出: 2
提示:
- 1 <= nums.length <= 2 * 10 ^4
- -1000 <= nums[i] <= 1000
- -10 ^ 7 <= k <= 10 ^7
分析
常规解法
求「和为 K 的子数组」,一般容易想到的暴力解法如下:
- 子数组由左右边界确定
- 固定左边界i,不断移动右边界j,求出所有左边界为i的子数组的和,如果子数组的和 = k,则计数+1
- i + 1,移动左边界,循环处理
代码实现也很简单,通过两层循环实现:
func subarraySum(nums []int, k int) int {
count := 0
l := len(nums)
// 子数组的左边界
for i := 0; i < l; i++ {
tempSum := 0
// 子数组的右边界
for j := i; j < l; j++ {
tempSum += nums[j]
// 满足条件,则计数+1
if tempSum == k {
count++
}
}
}
return count
}
时间复杂度:O(n ^ 2),两层循环,所以时间复杂度是n的平方
空间复杂度:O(1),常数额外空间
优化
暴力解法的问题就是时间复杂度太高,还有优化的空间
计算子数组的和,一般有两种方法:
- 加法,元素累加得到子数组的和
- 减法,子数组的和 = 大数组的和 - 小数组的和
上面的暴力解法,就是做加法,通过元素累加得到子数组的和,时间复杂度为O(m),m为子数组元素个数
还有一种方法是做减法,子数组的和 = 大数组的和 - 小数组的和
和加法对比,减法只要知道大数组的和、小数组的和,就可以O(1)时间复杂度计算出子数组的和,减少重复计算
还有一个需要解决的问题:怎么知道大数组的和、小数组的和?
很简单,只要循环一遍数组,累加计算即可
这种n个元素累加得到的结果,就是前缀和,新的数组就是前缀和数组
使用前缀和解决「和为K的子数组」问题
子数组的和 = 大数组的和 - 小数组的和
- 其中大数组的和、小数组的和,都可以通过前缀和得到
- 子数组的和 = K
所以我们的解题思路也很清晰:
- 遍历数组,计算当前下标的前缀和prefixSum
- 大数组的和 = prefixSum,子数组的和 = k,prefixSum - k = 小数组的和,这样的小数组的和如果存在,则结果计数加上对应小数组的和出现的次数
- 为了统计和快速查找小数组的和以及对应出现的次数,可以使用prefixSumMap来存储,key就是前缀和,value就是对应前缀和出现的次数
- 然后把前缀和prefixSum保存到prefixSumMap中
- 循环处理每一个元素
举例:前缀和prefixSum = 10,k = 4,则prefixSum - k = 6,如果前面prefixSum = 6出现过2次,则结果计数+2
特别注意
0个元素的前缀和也需要保存到prefixSumMap,也就是初始化时记得添加prefixSumMap[0] = 1
举例:前缀和prefixSum = 4,k = 4,则prefixSum - k = 0,此时对应的子数组从0下标开始
代码
func subarraySum(nums []int, k int) int {
count := 0
// 使用map来保存前缀和,key就是前缀和,value就是前缀和出现的次数
prefixSumMap := make(map[int]int)
// 0个元素的和为0,出现的次数为1
// 此时对应的子数组从0下标开始
prefixSumMap[0] = 1
prefixSum := 0
for _, num := range nums {
// 计算前缀和
prefixSum += num
// 需要查找的小数组的和
findPrefixSum := prefixSum - k
c, ok := prefixSumMap[findPrefixSum]
if ok {
// 存在,则更新计数
count += c
}
// 前缀和次数+1
prefixSumMap[prefixSum] = prefixSumMap[prefixSum] + 1
}
return count
}
时间复杂度:O(n),一次循环
空间复杂度:O(n),使用prefixSumMap来保存前缀和