「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战」。
题目链接: 303. 区域和检索 - 数组不可变
题目
给定一个整数数组 nums,处理以下类型的多个查询:
- 计算索引
left和right(包含left和right)之间的nums元素的 和 ,其中left <= right
实现 NumArray 类:
NumArray(int[] nums)使用数组nums初始化对象int sumRange(int i, int j)返回数组nums中索引left和right之间的元素的总和,包含left和right两点(也就是nums[left] + nums[left + 1] + ... + nums[right])
示例 1:
输入:
["NumArray", "sumRange", "sumRange", "sumRange"]
[[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
输出:
[null, 1, -1, -3]
解释:
NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1))
numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))
提示:
1 <= nums.length <= 104,注意这里不是我写错了,而是目前这题nums.length需要大于等于1(但是题目没改,如果我理解错了请评论告诉我修正),既不需要考虑空数组的问题-105 <= nums[i] <= 1050 <= i <= j < nums.length- 最多调用
10^4次sumRange方法
代码模板
/**
* @param {number[]} nums
*/
var NumArray = function (nums) {};
/**
* @param {number} left
* @param {number} right
* @return {number}
*/
NumArray.prototype.sumRange = function (left, right) {};
解法
首先分析一下题目,区域检索,就是数组一个区域检索出来,然后求总和,那这题其实就有思路了呀,检索我熟,我可是 for 循环集大成者,所有检索我都先暴力一波 for 循环,甭管它什么简单题中等题困难题,区域也好解决,都说了是集大成者,我做一个限制就行了嘛,非常简单啊,所以我这个 sumRange 可以这样写
var NumArray = function (nums) {
this.nums = nums;
};
NumArray.prototype.sumRange = function (left, right) {
let res = 0;
for(let i = left; i <= right; i++) {
res += this.nums[i];
}
return res;
};
不愧是简单题,我一个 for 循环就弄出来了
但是啊,要注意,有个小细节差点被漏掉,它那短短四行的提示,最后一个竟然故意的去说了最多调用 104 次 sumRange 方法,不是 leetcode 你调这么多次干嘛?
但回到 sumRange 本身,我们用 for 循环实现的时间复杂度是 O(n),这意味着我们在 leetcode 的极限测压下,可能时间复杂度会达到 O(10^4*n),这个时候我们就要考虑优化一下 sumRange
因此我们的解法必须要兼顾下面两个核心点
- 区域检索求和
sumRange时间复杂度要尽可能的小
解法
没错,文章来到了今天的主角,前缀和,前缀和是个啥玩意呢?我们先对它这个示例一,演示一波前缀和,看下图
看明白没有?前缀和就是原数组前 n 个和的结果,它的公式就是下面这个
i >= 1, preSum[0] = 0
preSum[i] = nums[0] +...+ nums[i-1]
比如:
preSum[3] = nums[0] + nums[1] + nums[2] = -2 + 0 + 3 = 1
为什么 i >= 1 和 preSum[0] = 0?这实际上是为了方便计算前 n 个和,以及存储,当然这都是细节问题,不同的人写的会不一样,但是最重要的是前缀和的理解,上面的过程其实也相当于下面这个
也就是下面的新公式,为题解的构建 preSum 的核心代码
i >= 1, preSum[0] = 0
preSum[i] = preSum[i-1] + nums[i-1]
比如:
preSum[3] = preSum[2] + nums[2] = -2 + 3 = 1
前缀和就是存放前 n 个和的结果,for 循环遍历中的中间结果的存放,从 0 -> 0,0 -> 1,。。。0 -> n,将这个结果存放起来帮助我们快速的得到 sumRange 的值,而不是每次都去 for 循环一次
可能这个时候你们有些疑问了,如果是 left = 1,right = 3,那 1 -> 2,2 -> 3 的这种中间结果怎么不去计算呢?原因是没必要,我们再次回到前缀和的公式,并带入 left 和 right
preSum[right + 1] = nums[0] + ... + nums[right]
preSum[left] = nums[0] + ... + nums[left - 1]
preSum[right + 1] - preSum[left] = nums[left] + ... + nums[right]
所以前缀和只需要 0 -> n 的中间结果,就可以得到所有的区域检索总和值
而解题代码就是下面这样
var NumArray = function(nums) {
const preSum = new Array(nums.length + 1).fill(0);
for(let i = 1; i < preSum.length; i++) {
preSum[i] = preSum[i-1] + nums[i-1];
}
this.preSum = preSum;
};
NumArray.prototype.sumRange = function(left, right) {
return this.preSum[right+1] - this.preSum[left];
};
总结
前缀和是利用了一次遍历数组的中间结果,作为后序计算特定的需求的基础值,并借此快速解决需求,这种思路像在 01背包,和动态规划都有用到,虽然303是简单题,但是内部的理念还是可以去深入了解的