刷题笔记,学有所得:两数之和的优化思想

96 阅读2分钟

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 * 104
  • 1000 <= nums[i] <= 1000
  • 107 <= 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]-k i < 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)