力扣刷题记录-前缀数组

前缀和数组

前缀和主要适⽤的场景是原始数组不会被修 改的情况下,频繁查询某个区间的累加和。

力扣 303. 区域和检索 - 数组不可变

原题链接
在这里插入图片描述

看到这题的第一反应应该都是每一次sumRange里面都执行一遍for循环,i从left开始,直到right为止,用一个sum对遍历到的每一个nums[i]进行累加和计算。

class NumArray {
	private int[] nums;
	public NumArray(int[] nums) {
		this.nums = nums;
	}
	
	public int sumRange(int left, int right) {
		int res = 0;
		for (int i = left; i <= right; i++) {
			res += nums[i];
		}
		return res;
	}
}

但是如果这样写的话,如果多次调用sumRange方法,那么每次调用此方法的时间复杂度都是O(n),这样的效率就比较低,需要想个方法把时间复杂度降到O(1),这就需要用到前缀和的方法了。

用一个preSum数组记录nums数组的前缀和,其中preSum[i]为nums[0]到nums[i-1]的元素和:
图片来自labuladong公众号
(图片来自labuladong公众号)

class NumArray {
    //用于计算nums数组的前缀和,preSum[i]为nums[0]到nums[i-1]的元素和
    private int[] preSum;
    public NumArray(int[] nums) {
        //preSum[0] = 0,便于计算累加和
        preSum=new int[nums.length+1];  
        for(int i=1;i<preSum.length;i++){
            preSum[i]+=preSum[i-1]+nums[i-1];
        }
    }

    //计算[left,right]区间内元素和
    public int sumRange(int left, int right) {
        return preSum[right+1]-preSum[left];
    }
}
/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray obj = new NumArray(nums);
 * int param_1 = obj.sumRange(left,right);
 */

这样每次调用sumRange时,只需进行减法运算即可算出区间元素和,其实这也是一种空间换时间的方法,多用了一个preSum数组进行记录,换来每次调用方法的高效率。


LeetCode 560. 和为 K 的子数组

原题链接

2023.05.31 一刷

思路:

  • 思路1:暴力枚举
      i用于遍历nums,j从i开始,向后遍历,用sum进行累加i~j之间的总和,当sum==k时,负责计数的count+1。

时间:双重for循环–O(n^2)–用时1649ms,击败23.53%
空间:无需额外数组空间–O(1)–内存消耗43.8MB,击败86.46%

代码如下:

//1.暴力枚举,时间O(n^2),空间O(1)
class Solution {
    public int subarraySum(int[] nums, int k) {
        int count=0;
        for(int i=0;i<nums.length;i++){
            int sum=0;
            for(int j=i;j<nums.length;j++){
                sum += nums[j];
                count += sum==k ? 1:0; 
            }
        }
        return count;
    }
} 
  • 思路2:前缀和数组加速区间和计算
      第一遍先遍历nums,用前缀和数组存下每个位置的前缀和,然后在双重循环枚举时,直接用前缀和数组计算i~j之间的和。

时间:O(n^2)–用时1961ms,击败5.03%
空间:O(n)–内存消耗43.7MB,击败88.31%

代码如下:

//2.前缀和数组加速求i~j之间的sum,时间O(n^2),空间O(n)
class Solution {
    public int subarraySum(int[] nums, int k) {
        int[] preSum=new int[nums.length+1];
        // preSum[i]为nums[0~i-1]的区间和,preSum[0]初始就为0,空置
        for(int i=1;i<nums.length+1;i++){
            preSum[i]=preSum[i-1]+nums[i-1];
        }
        int count=0;
        for(int i=0;i<nums.length;i++){
            for(int j=i;j<nums.length;j++){
                // i~j之间的和:preSum[j+1]-preSum[i]
                count+= preSum[j+1]-preSum[i]==k ? 1:0;
            }
        }
        return count;
    }
} 
  • 思路3:前缀和+HashMap
      遍历数组nums,计算从第0个元素到当前元素nums[i]的和,用哈希表保存出现过的累积和preSum的次数。如果preSum - k在哈希表中出现过,则代表从当前下标i往前有连续的子数组的和为k。

时间:只需要遍历nums一次–O(n)–用时22ms,击败89.7%
空间:需要用hashmap存储前缀和–O(n)–内存消耗44.9MB,击败48.71%

官方题解:
在这里插入图片描述
代码如下:

// 3.前缀和+HashMap
class Solution {
    public int subarraySum(int[] nums, int k) {
        // key存前缀和,value存对应前缀合出现的次数
        HashMap<Integer,Integer> hashmap=new HashMap<>();
        int preSum=0;
        int count=0;
        hashmap.put(0,1);//这句很重要,原因看下面注释
        for(int i=0;i<nums.length;i++){
            // preSum记录nums[0~i]之间的和
            preSum+=nums[i];
            // preSum[i]-preSum[j-1]=k,包含preSum-k键值对说明nums[j~i]的区间和为k,此时需要看0~i之间有多少次前缀和为preSum[j-1],count加上对应次数即可
            // put(0,1)补上了nums[0~i]区间和为k的情况(preSum=k),此时count+1
            if(hashmap.containsKey(preSum-k)){
                count+=hashmap.get(preSum-k);
            }
            hashmap.put(preSum,hashmap.getOrDefault(preSum,0)+1);
        }
        return count;
    }
} 

力扣 304. 二维区域和检索 - 矩阵不可变(同剑指 Offer II 013. 二维子矩阵的和)

原题链接
在这里插入图片描述

思路相比上一题303需要再复杂一点点,前缀和数组preSum的每一个元素perSum[i][j]为“以原点为左上角起点,到右下角matrix[i-1][j-1]之前矩形区域 ”的累加值。

利用preSum进行区域和计算的方法:
在这里插入图片描述
图片来自labuladong公众号。

代码如下:

//二维前缀和
//时间复杂度:初始化 O(mn),每次检索 O(1),其中m和n分别是矩阵matrix的行数和列数。
//空间O(mn)
class NumMatrix {
    private int[][] preSum;
    public NumMatrix(int[][] matrix) {
        int m=matrix.length,n=matrix[0].length;
        preSum=new int[m+1][n+1];
        for(int i=1;i<=m;i++)
            for(int j=1;j<=n;j++){
                preSum[i][j]=preSum[i][j-1]+preSum[i-1][j]-preSum[i-1][j-1]+matrix[i-1][j-1];
            }
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        return preSum[row2+1][col2+1]+preSum[row1][col1]-preSum[row1][col2+1]-preSum[row2+1][col1];
    }
}

/**
 * Your NumMatrix object will be instantiated and called as such:
 * NumMatrix obj = new NumMatrix(matrix);
 * int param_1 = obj.sumRegion(row1,col1,row2,col2);
 */

这题和剑指 Offer II 013. 二维子矩阵的和是一样的,可以之后再做这题熟练一下。


力扣 1314. 矩阵区域和

原题链接
在这里插入图片描述
理解题意:首先answer矩阵的每个元素都是在一个和k有关的、mat矩阵一定范围内的矩阵元素和。

比如mat = [1,2,3],[4,5,6],[7,8,9]],k=1;

answer[0][0]就是表示在mat矩阵中,行号在[i-k,i+k]范围内,列号在[j-k,j+k]范围内的元素和,即行号范围[-1,1](实际是[0,1],因为要在mat矩阵范围内),列号范围[-1,1](实际是[0,1])的所有元素合,即answer[0][0]=1+2+4+5=12;

同理,对于每一对i,j,都有对应范围的mat矩阵区域,每个answer[i][j]都是对应区域的元素和。只是需要注意区域范围在mat矩阵内(行:0-mat.length,列:0-mat[0].length)

也就是需要求m*n次二维矩阵的区域元素和,这是不是就可以联想到304. 二维区域和检索 - 矩阵不可变这题,它就是让我们编写程序用于求取指定区域的元素和,所以这题可以直接使用它的代码:

//借用304题现成的代码
class Solution {
    public int[][] matrixBlockSum(int[][] mat, int k) {
        int m=mat.length,n=mat[0].length;
        int[][] answer =new int[m][n];//结果数组
        int x1,y1,x2,y2;
        NumMatrix nummatrix=new NumMatrix(mat);
        for(int i=0;i<m;i++)
            for(int j=0;j<n;j++){//针对每一对i、j找到对应矩阵区域范围
                x1=Math.max(i-k,0);//防止i-k小于数组索引边界
                y1=Math.max(j-k,0);
                x2=Math.min(i+k,m-1);//防止i+k超出数组索引边界
                y2=Math.min(j+k,n-1);
                answer[i][j]=nummatrix.sumRegion(x1,y1,x2,y2);
            }
        return answer;
    }
}
//304题代码
class NumMatrix {
    private int[][] preSum;
    public NumMatrix(int[][] matrix){
        int m=matrix.length,n=matrix[0].length;
        preSum=new int[m+1][n+1];
        //前缀和数组赋值
        for(int i=1;i<=m;i++)
            for(int j=1;j<=n;j++){
                preSum[i][j]=preSum[i-1][j]+preSum[i][j-1]-preSum[i-1][j-1]+matrix[i-1][j-1];
            }
    }
    //用于输出矩阵区域元素和
    public int sumRegion(int row1,int col1,int row2,int col2){
        return  preSum[row2+1][col2+1]+preSum[row1][col1]-preSum[row2+1][col1]-preSum[row1][col2+1];
    }
}

当然,也可以不用这么长的代码:

//合起来写法
class Solution {
    public int[][] matrixBlockSum(int[][] mat, int k) {
        int m=mat.length,n=mat[0].length;
        int[][] preSum=new int[m+1][n+1];//前缀和数组
        int[][] answer =new int[m][n];//结果数组
        int x1,y1,x2,y2;
        //前缀和数组赋值
        for(int i=1;i<=m;i++)
            for(int j=1;j<=n;j++){
                preSum[i][j]=preSum[i-1][j]+preSum[i][j-1]-preSum[i-1][j-1]+mat[i-1][j-1];
            }
        //计算区域和
        for(int i=0;i<m;i++)
            for(int j=0;j<n;j++){
                x1=Math.max(i-k,0);//防止i-k小于数组索引边界
                y1=Math.max(j-k,0);
                x2=Math.min(i+k,m-1);//防止i+k超出数组索引边界
                y2=Math.min(j+k,n-1);
                answer[i][j]=preSum[x2+1][y2+1]+preSum[x1][y1]-preSum[x2+1][y1]-preSum[x1][y2+1];//求区域元素和
            }
        return answer;
    }
}r[i][j]=preSum[x2+1][y2+1]+preSum[x1][y1]-preSum[x2+1][y1]-preSum[x1][y2+1];//求区域元素和
            }
        return answer;
    }
}

这题需要注意的就是题目的理解,以及对i-k、i+k、j-k、j+k的范围用Math.max/min作限定。


力扣 1352. 最后 K 个数的乘积

原题链接
在这里插入图片描述

这题可以像求前缀和一样的方式算出前缀积,前缀积 pre[i] 表示前i个数的乘积,最后k个数的乘积就是pre[n]/pre[n-k],不过对0的存在要特别注意,因为除0是不允许的。

//前缀积,add和getProduct时间O(1);空间O(n),n为前缀积list
class ProductOfNumbers {
    List<Integer>list =new ArrayList<>();//记录前缀积
    //初始化
    public ProductOfNumbers() {
        list.add(1);//初始化加入1,方便计算乘积
    }
    
    public void add(int num) {
        if(num==0){//很关键的一步,遇到0直接清空前缀积
            list.clear();
            list.add(1);
            return;
        }//能走到这说明num!=0
        int n=list.size();//方便调用get方法
        list.add(num*list.get(n-1));//保存当前num加入后的前缀积

    }
    
    public int getProduct(int k) {
        int n=list.size();
        //list剩余的实际元素个数不超过k,说明在倒数k个内碰到了0元素
        //导致list清空,倒数k个内有0,返回值必定为0
        if(k>n-1){//因为第一个元素为初始化的1,不计入实际个数
            return 0;
        }//能走到这说明list剩余元素比k多,那直接用公式计算即可
        return list.get(n-1)/list.get(n-1-k);
    }
}

/**
 * Your ProductOfNumbers object will be instantiated and called as such:
 * ProductOfNumbers obj = new ProductOfNumbers();
 * obj.add(num);
 * int param_2 = obj.getProduct(k);
 */

327. 区间和的个数(比较难,需要归并排序知识,先放着,完成315之后再来做这题)


前缀积

力扣 238. 除自身以外数组的乘积(同剑指Offer 66. 构建乘积数组)

原题链接
在这里插入图片描述
在评论区看到一个很简单明了的思路举例,来自Carol:
在这里插入图片描述
2023.06.03 一刷

思路:
1.直观的前缀积数组

  • 先从左到右遍历nums,L[i]记录nums[i]左侧所有数乘积,L[0]=1(nums[0]左侧无数,初始化为1);
  • 然后再从右向左遍历,R[i]记录nums[i]右侧所有数乘积,R[n-1]=1(nums[n-1]右侧无数);
  • 最后的res[i]=L[i]*R[i];

时间O(n),空间O(n)

代码如下:

//  1.直观的前缀积数组,时间O(n),空间O(n)
class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n=nums.length;
        int[] L=new int[n];
        int[] R=new int[n];
        int[] res=new int[n];

        L[0]=1;
        // 从左到右遍历,求左边乘积
        for(int i=1;i<n;i++){
            L[i]=L[i-1]*nums[i-1];
        }

        R[n-1]=1;
        // 从右到左遍历,求右边乘积
        for(int i=n-2;i>=0;i--){
            R[i]=R[i+1]*nums[i+1];
        }

        // 从左到右遍历nums,求出每个nums[i]的结果
        for(int i=0;i<n;i++){
            res[i]=L[i]*R[i];
        }
        return res;
    }
}

2.前缀积数组优化

  • 输出数组不被视为额外空间,所以可以用res[i]先从左到右遍历nums,记录每个nums[i]左侧乘积;
  • 再从有到左遍历nums,每个位置最后的结果就是res[i]乘以nums[i]右侧所有数的乘积,这个乘积在向左遍历的过程中可以用一个变量R来维护,每到一个位置更新一次即可。
  • 这样最终只需要O(1)的时间复杂度(除去输出数组使用的空间)

时间O(n),空间O(1)

代码如下:

//2.优化前缀积数组,时间O(n),空间O(1)
class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n=nums.length;
        int[] res=new int[n];

        res[0]=1;
        // 从左到右遍历,res[i]记录nums[i]左侧乘积
        for(int i=1;i<n;i++){
            res[i]=res[i-1]*nums[i-1];
        }

        int R=1;
        // 从右到左遍历,res[i]记录最终结果
        for(int i=n-1;i>=0;i--){
            res[i]=res[i]*R;
            R*=nums[i];
        }
        return res;
    }
}

此题同剑指 Offer 66. 构建乘积数组,做完此题之后,可以到剑指offer里再写一遍回顾思想。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值