560. 和为 K 的子数组
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 *该数组中和为 k ***的连续子数组的个数 。
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
提示:
1 <= nums.length <= 2 * 1041000 <= nums[i] <= 1000107 <= k <= 107
错误思路
看见连续子数组,这题一上来,我就想通过滑动窗口去解题。
后面发现犯了很大错误,因为这不是有序的数组,没办法利用类似二分法的双指针方法去收缩边界(不知道什么时候应该收缩)
nums[l] < nums[r],所以和大了就移动左边界,删除窗口中的元素
标准解题思路
暴力法
固定住左边界,枚举出可能的所有右边界,进行确认区间中的连续子数组和是否满足target
很自然写出两层for循环
func subarraySum(nums []int, k int) int {
res := 0
for i := 0; i < len(nums); i++ {
tmp := 0
for j := i; j < len(nums); j++ {
tmp += nums[j]
if tmp == k {
res++
}
}
}
return res
}
时间复杂度:O(n^2)
空间复杂度:O(1)
肯定超时啦~
前缀和优化
说优化不太恰当,因为将空间复杂度提升为 O(n)了,但是思路很好,一下子没想到!
其实我们会发现,对于每一个i,都重复做 nums[i] + … + nums[j]的操作,其实我们在i+1时,nums[i+1] + … + nums[j]已经在i时计算过了
当时,我就一直在想,如何优化这一步操作,其实应该想到的,快速计算区间和,应该使用前缀和的优化方法!
所以我们写出:
func subarraySum(nums []int, k int) int {
preSum := make([]int, len(nums)+1)
preSum[0] = 0
for i := range nums {
preSum[i+1] = preSum[i] + nums[i]
}
res := 0
for i := 0; i < len(nums); i++ {
for j := i+1; j <= len(nums); j++ {
if preSum[j]-preSum[i] == k {
res++
}
}
}
return res
}
令我意外的是,这次提交很勉强通过了,实际上时间复杂度还是O(n^2),空间复杂度提升为O(n)
前缀和+哈希表(两数之和的思想)
我们可以发现,在内层for循环中,一直做的一件事情是 preSum[j]-preSum[i] == k
这和两数之和:nums[i]+nums[j] == k 没有差别啊!!
所以我们可以考虑用哈希表,然后去遍历preSum(前缀和数组)
也就是我们要找在之前是否出现过
preSum[i]即preSum[j]-ki < j 等同于两数之和找nums[i]即k-nums[i]i < j 注意:由于这是减法而不是加法,所以顺序很重要!!千万不要找成 preSum[j]了!这样会出错
func subarraySum(nums []int, k int) int {
preSum := make([]int, len(nums)+1)
preSum[0] = 0
for i := range nums {
preSum[i+1] = preSum[i] + nums[i]
}
res := 0
ht := make(map[int]int)
for j := range preSum {
if cnt, ok := ht[preSum[j]-k]; ok {
res += cnt
}
ht[preSum[j]] += 1
}
return res
}
回到话题,为什么说是前缀和优化?
- 我想这是因为使用前缀和,我们将一个区间的累加求和问题O(weight),简化为了O(1)! 虽然在上述题目中,似乎改变了计算方法(tmp+=nums[j] ⇒ preSum[j]-preSum[i]),但时间复杂度没变
- 但是这样就可以结合哈希表对搜索进行优化
还能优化吗?
当然!空间复杂度其实可以优化一下
我们计算preSum其实就说粗暴的累加,每次对 preSum[j] 找 preSum[i] 也不过是在j之前有没有出现 i 满足等式条件!
func subarraySum(nums []int, k int) int {
res := 0
ht := make(map[int]int)
pre_j := 0
ht[pre_j] = 1
for _, num := range nums {
pre_j += num
if cnt, ok := ht[pre_j-k]; ok {
res += cnt
}
ht[pre_j] += 1
}
return res
}
总结
两数之和的精髓之处在于,将o(n^2)复杂度(固定某一点,寻找后面的下一点,以满足某一条件)通过哈希表记录先前访问过的节点,优化为o(n)