一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第4天,点击查看活动详情。
题目描述
给你一个数组 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]
思路
这题是数据结构相关的题目。我们还是从最直接的思路出发,想想如果我们只使用array,会存在什么问题。
- 初始化,时间复杂度是O(n)
- 更新操作,时间复杂度是O(1)
- sumRange,时间复杂度是O(n)
初始化没什么可说的,更新也已经是O(1)了,存在优化空间的就是sumRange。三叶姐的题解介绍了树状数组和线段树的方法,需要良好数据结构基础。我们这里介绍一种朴素的分段的方法。
我们把原始数组分成n个段,每个段的大小都固定为boxSize,当然,最后一个段可能有零头的存在,所以大小在0~s之间。这样分好之后,我们先进行一次预处理,把每个段的和都求出来,保存在sums数组中。然后我们分别来看更新和sumRange操作的过程。
更新操作
更新的num[i]的时候,同时也要更新num[i]所在的段的和sums[i/boxSize]。这里更新sums[i/boxSize]的时候,不需要对这个段重新求和,只要在原始段和的基础上,加上i位置新值和旧值的差就好,所以我们这里调换一下顺序,先更新sums[i/boxSize],这时候num[i]还是旧值,可以把更新操作的时间复杂度维持在O(1)
sumRange
首先判断left和right所在的段:
- 同一段 没什么可说的,直接从left直接累加到right即可,注意包含right,是一个前闭后闭的区间。这种情况的时间复杂度是O(boxSize)
- 不同段 这时候可以把和分成3个部分:
- sumLeft:left所在区间从left到区间末的和
- sumMid:left和right中间所有的区间和
- sumRight:right所在区间从开始到right的和
例如下图就是当boxSize=4时,求2-8和的拆分
这种情况的时间复杂度是O(boxSize + len/boxSize + boxSize),即O(boxSize + len/boxSize)
怎么确定boxSize的值
根据上面最大的时间复杂度O(boxSize + len/boxSize),即求出使得这个表达式最小的boxSize的值。
令 a=sqrt(boxSize),b=sqrt(len/boxSize)
boxSize + len/boxSize = a^2 + b^2 >= aab = 2*len
当切仅当a=b时等号等成立,所以 boxSize = sqrt(len)时,表达式取得最小值
Java版本代码
class NumArray {
private int[] nums;
private int[] sums;
private int boxSize;
public NumArray(int[] nums) {
this.nums = nums;
int len = nums.length;
boxSize = (int)Math.sqrt(len);
sums = new int[len/boxSize+1];
for (int i = 0; i < len; i++) {
sums[i/boxSize] += nums[i];
}
}
public void update(int index, int val) {
sums[index/boxSize] += val - nums[index];
nums[index] = val;
}
public int sumRange(int left, int right) {
int b1 = left / boxSize;
int b2 = right / boxSize;
if (b1 == b2) {
int sum = 0;
for (int i = left; i <= right; i++) {
sum += nums[i];
}
return sum;
}
int sumLeft = 0;
for (int i = left; i < (b1+1)*boxSize; i++) {
sumLeft += nums[i];
}
int sumMid = 0;
for (int i = b1+1; i < b2; i++) {
sumMid += sums[i];
}
int sumRight = 0;
for (int i = b2*boxSize; i <= right; i++) {
sumRight += nums[i];
}
return sumLeft + sumMid + sumRight;
}
}
/**
* Your NumArray object will be instantiated and called as such:
* NumArray obj = new NumArray(nums);
* obj.update(index,val);
* int param_2 = obj.sumRange(left,right);
*/