Leetcode560-和为K的子数组

题目描述

给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。

示例 1 :

输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
说明 :

数组的长度为 [1, 20,000]。
数组中元素的范围是 [-1000, 1000] ,且整数 k 的范围是 [-1e7, 1e7]。

题解

算出一共有几个和为 k 的子数组,把所有子数组都穷举出来,算它们的和,看看谁的和等于 k 就行了。关键是,如何快速得到某个子数组的和,比如说给你一个数组 nums,让你实现一个接口 sum(i, j),这个接口要返回 nums[i…j] 的和,而且会被多次调用,你怎么实现这个接口呢?

因为接口要被多次调用,显然不能每次都去遍历 nums[i…j],有没有一种快速的方法在 O(1) 时间内算出 nums[i…j] 呢?这就需要前缀和技巧了。
一、什么是前缀和
对于一个给定的数组 nums,我们额外开辟一个前缀和数组进行预处理:

int n = nums.length;
// 前缀和数组
int[] preSum = new int[n + 1];
preSum[0] = 0;
for (int i = 0; i < n; i++)
    preSum[i + 1] = preSum[i] + nums[i];

在这里插入图片描述
这个前缀和数组 preSum 的含义也很好理解,preSum[i] 就是 nums[0…i-1] 的和。那么如果我们想求 nums[i…j] 的和,只需要一步操作 preSum[j+1]-preSum[i] 即可,而不需要重新去遍历数组了。

回到这个子数组问题,我们想求有多少个子数组的和为 k,借助前缀和技巧很容易写出一个解法:

int subarraySum(int[] nums, int k) {
    int n = nums.length;
    // 构造前缀和
    int[] sum = new int[n + 1];
    sum[0] = 0; 
    for (int i = 0; i < n; i++)
        sum[i + 1] = sum[i] + nums[i];
    
    int ans = 0;
    // 穷举所有子数组
    for (int i = 1; i <= n; i++)
        for (int j = 0; j < i; j++)
            // sum of nums[j..i-1]
            if (sum[i] - sum[j] == k)
                ans++;

    return ans;
}

在这里插入图片描述
二、优化解法
前面的解法有嵌套的 for 循环:

for (int i = 1; i <= n; i++)
    for (int j = 0; j < i; j++)
        if (sum[i] - sum[j] == k)
            ans++;

第二层 for 循环在干嘛呢?翻译一下就是,**在计算,有几个 j 能够使得 sum[i] 和 sum[j] 的差为 k。**毎找到一个这样的 j,就把结果加一。
我们可以把 if 语句里的条件判断移项,这样写:

if (sum[j] == sum[i] - k)
    ans++;

优化的思路是:我直接记录下有几个 sum[j] 和 sum[i] - k 相等,直接更新结果,就避免了内层的 for 循环。我们可以用哈希表,在记录前缀和的同时记录该前缀和出现的次数。
比如说下面这个情况,需要前缀和 8 就能找到和为 k 的子数组了,之前的暴力解法需要遍历数组去数有几个 8,而优化解法借助哈希表可以直接得知有几个前缀和为 8。
在这里插入图片描述
这样,就把时间复杂度降到了 O(N),是最优解法了。

代码实现

class Solution {
    public int subarraySum(int[] nums, int k) {
        //hashmap用于记录每个前缀和出现的次数
        HashMap<Integer,Integer> map = new HashMap<>();
        int preSum=0;
        int count=0;
        //因为可能出现nums[0]=k或者从0开始的连续子数组和等于k的情况,所以map.put(0,1)
        map.put(0,1);
        for(int i=0;i<nums.length;i++){
            preSum+=nums[i];
            if(map.containsKey(preSum-k)){
                //因为可能出现多个前缀和等于preSum-k的情况,所以count+=map.get(preSum-k)而不是count++
                count += map.get(preSum-k);
            }
            if(map.containsKey(preSum)){
                map.put(preSum,map.get(preSum)+1);
            }else{
                map.put(preSum,1);
            }
        }
        return count;
    }
}

关于mp.put(0, 1); 这一行的作用就是为了应对 nums[0] +nums[1] + … + nums[i] == k 的情况的, 也就是从下标 0 累加到下标 i, 举个例子说明, 如数组 [1, 2, 3, 6], 那么这个数组的累加和数组为 [1, 3, 6, 12] 如果 k = 6, 假如map中没有预先 put 一个 (0, 1) , 如果此时我们来到了累加和为 6 的位置, 这时map中的情况是 (1, 1), (3, 1), 而 mp.containsKey(pre - k) , 这时 pre - k 也就是 6 - 6 = 0, 因为 map 中没有 (0, 1) 所以 count 的值没有加一, 其实这个时候我们就是忽略了从下标 0 累加到下标 i 等于 k 的情况, 我们仅仅是统计了从下标大于 0 到某个位置等于 k 的所有答案
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值