一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情。
前言
今天的题目为中等,第一次触碰到对于数组求和的时间复杂度优化,没有理解大佬们的题解中的线段树是什么意思,只能退而求其次选择了官方解答中的分段处理的思想。
每日一题
今天的题目是 307. 区域和检索 - 数组可修改,难度为中等
-
给你一个数组 nums ,请你完成两类查询。
-
其中一类查询要求 更新 数组 nums 下标对应的值
-
另一类查询要求返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的 和 ,其中 left <= right
-
实现 NumArray 类:
-
NumArray(int[] nums) 用整数数组 nums 初始化对象
-
void update(int index, int val) 将 nums[index] 的值 更新 为 val
-
int sumRange(int left, int right) 返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的 和 (即,nums[left] + nums[left + 1], ..., nums[right])
示例 1:
输入:
["NumArray", "sumRange", "update", "sumRange"]
[[[1, 3, 5]], [0, 2], [1, 2], [0, 2]]
输出:
[null, 9, null, 8]
解释:
NumArray numArray = new NumArray([1, 3, 5]);
numArray.sumRange(0, 2); // 返回 1 + 3 + 5 = 9
numArray.update(1, 2); // nums = [1,2,5]
numArray.sumRange(0, 2); // 返回 1 + 2 + 5 = 8
提示:
- 1 <= nums.length <= 3 * 104
- -100 <= nums[i] <= 100
- 0 <= index < nums.length
- -100 <= val <= 100
- 0 <= left <= right < nums.length
- 调用 pdate 和 sumRange 方法次数不大于 3 * 104
题解
暴力解法
本来一看到题目,简单题,不就是在一个类里面新建两个修改数组的方法,然后也真的去做了,结果就是:
class NumArray {
constructor(arr) {
this.arr = arr;
}
update(index, val) {
this.arr[index] = val;
}
sumRange(left, right) {
let sum = 0;
for (let i = left; i <= right; i++) {
sum += this.arr[i];
}
return sum;
}
}
开始意识到,题目没有那么简单
分块处理
分块处理,参考了leetcode上官方解答的第一个方法,分块思考:
上面暴力解法会超时的原因主要在于,求和的次数过多的话,每次都去遍历全部的 left 到 right 浪费了太多的时间,所以就引出了一个想法,我们能否将整个的大数组,分为很多的小数组,每个小数组就用它们的和来表示,那么在最后一个求和的时候,如果碰到了 left 和 right 的范围很大,那么也不用去循环遍历,只要将之前分好的小数组的和加起来就好,这样在运行了多次求和函数特别是范围大的求和函数的情况下,能节省很多的浪费在循环上的时间复杂度
原本如果从 left 一直遍历到 right 那么时间复杂度为 O(n),在分块处理的方法中,我们把原数组的长度开根号,比如说原数组长度为9,那么我们就把它分为三个部分,每个部分用三个数的和表示,那么在最完美的情况下,能够将时间复杂度节省到 O(√n)
- 那么明确了方法,就要在一开始保存数组的时候开始进行处理,我们将题目给的原数组开根号,看看长度需不需要进行分段,需要的话就用一个新的数组保存下来。下面拿一个10个数的数组做个例子。
-
那么在我们对数组做了分块,那么修改方法需要修改的地方就变成了两个,一个是原数组,一个是新的分块数组中对应那一块的值。
-
重点就是修改方法,那么分块是怎么做到节省时间复杂度的呢,我们将 left 和right 做一个判断,在一个分块后的数组当中,最坏的情况就是会分为三个部分,一个是左边的一个块被拆分了:
右边也是同理,那么我们就需要分三块来处理这个的逻辑,一个是左边的那一块的值,一个是中间的所有快,一个是右边的被拆分的值,对应上面的,就是左边的部分是 索引为 2 的这一个元素,中间就是 [2,4,5] 组成的 11,右边就是 6 和 2 的值。
那么试想一下,在 left 和 right 的跨度够大的时候,中间的分块是不是就不用像用索引相加那样子求和 O(n) 次,而只需要 O(√n) 次,那么这就大大地节省了时间复杂度。
class NumArray {
constructor(nums) {
this.nums = nums;
const n = nums.length;
this.size = Math.floor(Math.sqrt(n));
this.newNums = new Array(Math.floor((n + this.size - 1) / this.size)).fill(0);
for (let i = 0; i < n; i++) {
this.newNums[Math.floor(i / this.size)] += nums[i];
}
}
update(index, val) {
this.newNums[Math.floor(index / this.size)] += val - this.nums[index];
this.nums[index] = val;
}
sumRange(left, right) {
const leftSize = Math.floor(left / this.size),
leftVal = left % this.size,
rightSize = Math.floor(right / this.size),
rightVal = right % this.size;
if (leftSize === rightSize) {
let newNums = 0;
for (let j = leftVal; j <= rightVal; j++) {
newNums += this.nums[leftSize * this.size + j];
}
return newNums;
}
let sum1 = 0;
for (let j = leftVal; j < this.size; j++) {
sum1 += this.nums[leftSize * this.size + j];
}
let sum2 = 0;
for (let j = 0; j <= rightVal; j++) {
sum2 += this.nums[rightSize * this.size + j];
}
let sum3 = 0;
for (let j = leftSize + 1; j < rightSize; j++) {
sum3 += this.newNums[j];
}
return sum1 + sum2 + sum3;
}
}