[前缀和-动态规划] 560. 和为K的子数组(一维前缀和 → 空间优化 → 前缀和+哈希表)

560. 和为K的子数组

题目链接:https://leetcode-cn.com/problems/subarray-sum-equals-k/


分类:动态规划(前缀和)、哈希表


在这里插入图片描述
.
.

什么是前缀和?

参考链接:前缀和 - by YocnZhao

一维前缀和

设置一个数组num和它的前缀和数组sum,他们满足以下公式:

sum[0] = num[0]
sum[1] = num[0] + num[1]
sum[2] = num[0] + num[1] + num[2]
...
即 sum[n]=num[1]+num[2]+...+num[n]。

一维前缀和的构造代码:

    public int[] oneDimen(int[] src) {
        int[] result = new int[src.length];
        for (int i = 0; i < result.length; i++) {
            if (i == 0) {
                result[i] = src[i];
            } else {
                result[i] = result[i - 1] + src[i];
            }
        }
        return result;
    }
二维前缀和

如下图:
在这里插入图片描述
使用一个二维数组prefixSum来保存前缀和,数组里的每一个位置表示原数组当前位置的左上方矩形的元素之和。

二维前缀和数组的构造需要分四种情况讨论:

  1. i == 0 && j == 0,直接赋值:prefixSum[0, 0] = src[0, 0];
  2. i == 0,最左边的一排,图中黄色部分,按垂直方向求和:prefixSum[0, j] = prefixSum[0, j-1] + src[0, j];
  3. j == 0,最上面一排,图中红色部分,按水平方向求和:prefixSum[i, 0] = prefixSum[i-1, 0] + src[i, 0];
  4. i !=0 || j != 0,图中绿色部分,prefixSum[i][j] = prefixSum[i - 1][j] + prefixSum[i][j - 1] + src[i][j] - prefixSum[i - 1][j - 1];

第4步中,绿色部分位置的前缀和等于以它为右下角的矩形的元素之和,我们不需要枚举矩形内的元素重新计算,可以利用现有的计算结果,如下图:
在这里插入图片描述
要计算prefixSum[2,2],就等于src[2,2]加上绿色框,加上红色框,再减去它们重叠的部分,其中绿色框的前缀和保存在prefixSum[2,1],红色框的前缀和保存在prefixSum[1,2],两个矩形的重叠部分前缀和保存在prefixSum[1,1],所以:

prefixSum[2,2] = prefixSum[1, 2] + prefixSum[2, 1] - prefixSum[1, 1] + src[2, 2];//注意:最后src[2,2]容易遗漏

二维前缀和的构造代码:

    public int[][] twoDimen(int[][] nums){
        int rows = nums.length, cols = nums[0].length;
        int[][] sum = new int[rows][cols];
        for(int i = 0; i < rows; i++){
            for(int j = 0; j < cols; j++){
                //矩阵第0个元素
                if(i == 0 && j == 0) sum[i][j] = nums[i][j];
                //矩阵第0行
                else if(i == 0) sum[i][j] = sum[i][j-1]+nums[i][j];
                //矩阵第0列
                else if(j == 0) sum[i][j] = sum[i-1][j]+nums[i][j];
                //矩阵其他位置
                else{
                    sum[i][j] = sum[i-1][j] + sum[i][j-1] + nums[i][j] - sum[i-1][j-1];
                }
            }
        }
        return sum;
    }
前缀和的应用(*****)

思路1:前缀和(O(N^2),O(N))

1、设置一个数组sum来保存nums[0,i]的和,即保存前缀和,其中i=0~n-1,则前缀和的计算公式为:

    nums[i~j]的和=sum[j+1]-sum[i],

例如:

nums=[1,1,1]
sum=[0,1,2,3]
则nums[0,1]的和=sum[2]-sum[0]=2

2、得到sum数组后,开始统计数组中和为k的连续子数组个数,对sum做双重遍历:

外层for-i从下标0开始,内层for-j从下标i+1开始,nums[i~j]的和=sum[j]-sum[i],如果和等于k,则计数器ret+1。

3、返回ret存放的最终结果。

实现代码:

class Solution {
    public int subarraySum(int[] nums, int k) {
        int len = nums.length;
        int[] sum = new int[len + 1];
        //构造前缀和数组
        for(int i = 1; i < len + 1; i++){
            sum[i] = sum[i - 1] + nums[i - 1];
        }
        int ret = 0;
        for(int i = 0; i < sum.length; i++){
            for(int j = i + 1; j < sum.length; j++){
                if(sum[j] - sum[i] == k) ret++;
            }
        }
        return ret;
    }
}
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(N)

思路2:思路1空间优化(O(N^2),O(1))

1、思路1使用了一个额外数组来存放前缀和,我们可以直接把前缀和存放在nums数组上,将空间复杂度降为O(1),但时间复杂度不变。
前缀和的计算公式和思路1相比需要做些修改:

    nums[0~i]的和存放在nums[i]上,所以nums[i]=nums[i-1]+nums[i],i从1开始。

2、nums数组处理完时,nums数组保存的就是一个个前缀和,和思路1相同,在nums上双重遍历,统计数组中和为k的连续子数组个数:

  • 如果i>0,则原数组nums[i~j]的和=现数组nums[j]-nums[i-1];
  • 如果i==0,则原数组nums[i~j]的和=现数组nums[j]。

实现代码:

class Solution {
    public int subarraySum(int[] nums, int k) {
        int len = nums.length;
        //构造前缀和数组
        for(int i = 1; i < len; i++){
            nums[i] = nums[i - 1] + nums[i];
        }
        int ret = 0;
        for(int i = 0; i < len; i++){
            for(int j = i; j < len; j++){
                if(i == 0){
                    if(nums[j] == k) ret++;
                }
                else if(nums[j] - nums[i - 1] == k) ret++;
            }
        }
        return ret;
    }
}
  • 时间复杂度:O(N^2)
  • 空间复杂度:O(1)

思路3:前缀和+哈希表(O(N),O(N),推荐)

1、遍历数组nums,计算前缀和sum,即sum=nums[0i]的和(其中i=0n-1),每一个前缀和都保存到哈希表中:

  • 如果第一次遇到一个前缀和值,则value置1;
  • 如果是重复的前缀和,则对应的value+1。

2、设置一个ret保存满足条件的连续子数组的数量,在遍历nums的同时统计ret:
对于每一个计算得到的前缀和sum,都做如下判断:

  • 判断sum-k是否存在于哈希表中,如果sum-k存在于哈希表中,说明从当前下标i处往前存在一个或多个和为k的连续子数组,ret加上sum-k对应的value,这里的value表示nums数组中连续子数组和为k的数量;
  • 判断sum是否等于k,如果等于k说明找到一个满足条件的子数组,ret+1,并将这个sum存入map中:
    • 如果sum存在于map中,则map中对应的value+1;
    • 如果sum不存在于map中,则直接插入,value置1.

实现代码:

class Solution {
    public int subarraySum(int[] nums, int k) {
        int sum = 0;//存放计算得到的前缀和
        int ret = 0;//统计满足条件的子数组个数作为最终结果
        //map的key=前缀和,value=出现的次数
        Map<Integer, Integer> map = new HashMap<>();
        for(int i = 0; i < nums.length; i++){
            sum += nums[i];
            //如果sum-k存在于map中,说明在i之前存在一个或多个和为k的连续子数组,ret加上sum-k对应的value
            if(map.containsKey(sum - k)) ret += map.get(sum - k);
            //如果sum等于k说明找到一个满足条件的子数组,ret+1
            if(sum == k) ret++;
            //将每个前缀和都加入map中
            int val = map.getOrDefault(sum, 0);
            map.put(sum, val + 1);
        }
        return ret;
    }
}
  • 时间复杂度:O(N)
  • 空间复杂度:O(N)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值