题目目录
前缀和数组
前缀和主要适⽤的场景是原始数组不会被修 改的情况下,频繁查询某个区间的累加和。
力扣 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公众号)
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里再写一遍回顾思想。