使用前缀和解决【560.和为 K 的子数组】问题

109 阅读3分钟

题目

给你一个整数数组 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 的子数组」,一般容易想到的暴力解法如下:

  1. 子数组由左右边界确定
  2. 固定左边界i,不断移动右边界j,求出所有左边界为i的子数组的和,如果子数组的和 = k,则计数+1
  3. i + 1,移动左边界,循环处理

image.png

代码实现也很简单,通过两层循环实现:

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),常数额外空间

优化

暴力解法的问题就是时间复杂度太高,还有优化的空间

计算子数组的和,一般有两种方法:

  1. 加法,元素累加得到子数组的和
  2. 减法,子数组的和 = 大数组的和 - 小数组的和

上面的暴力解法,就是做加法,通过元素累加得到子数组的和,时间复杂度为O(m),m为子数组元素个数

image.png

还有一种方法是做减法,子数组的和 = 大数组的和 - 小数组的和

和加法对比,减法只要知道大数组的和、小数组的和,就可以O(1)时间复杂度计算出子数组的和,减少重复计算

image.png 还有一个需要解决的问题:怎么知道大数组的和、小数组的和?

很简单,只要循环一遍数组,累加计算即可

这种n个元素累加得到的结果,就是前缀和,新的数组就是前缀和数组

image.png

使用前缀和解决「和为K的子数组」问题

子数组的和 = 大数组的和 - 小数组的和

  • 其中大数组的和、小数组的和,都可以通过前缀和得到
  • 子数组的和 = K

image.png 所以我们的解题思路也很清晰:

  1. 遍历数组,计算当前下标的前缀和prefixSum
  2. 大数组的和 = prefixSum,子数组的和 = k,prefixSum - k = 小数组的和,这样的小数组的和如果存在,则结果计数加上对应小数组的和出现的次数
  3. 为了统计和快速查找小数组的和以及对应出现的次数,可以使用prefixSumMap来存储,key就是前缀和,value就是对应前缀和出现的次数
  4. 然后把前缀和prefixSum保存到prefixSumMap中
  5. 循环处理每一个元素

举例:前缀和prefixSum = 10,k = 4,则prefixSum - k = 6,如果前面prefixSum = 6出现过2次,则结果计数+2

image.png

特别注意

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来保存前缀和