题目描述:
给你一个数组
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]
)
根据题目的意思,总共有两个方法要实现:单点更新和区间求和,这正好符合树状数组的基本功能,因此这是一道树状数组的裸题,那我们正好可以在这复习一下树状数组的知识。
显然,对于普通数组来说,单点更新的时间复杂度是 ,而区间求和的时间复杂度是 。如果我们想要加快对区间求和的速度,那一个很传统的做法就是求前缀和 ,此时区间 的和就是 。这样子区间求和的时间复杂度确实降到了 ,但由于单点更新时要同步更新所有该点之和的前缀和,因此单点更新的时间复杂度又变成了 。
这时候,想必聪明的你已经在考虑,是否有一种折中的方案,能够让单点更新和区间求和都不要太慢,比如都在 的样子呢?
按照这样的思路,我们可以把数组分成一个个块,假设每块的大小为 ,那么长度为 的数组就可以分成 个大小相同的块。之后,我们在单点更新时,只需要更新对应的数组块,而区间查询,也最多遍历两个块即可,确实达到了我们的预期。经过计算推导可以得出,当 时,复杂度最优,此时单点更新时间复杂度是 ,区间查询的时间复杂度是 。
这样的解法显然有些 “naive” 了,是否有更优的解法呢?答案是肯定的,这时就轮到大名鼎鼎的树状数组登场了!具体思路是用一个数组 维护若干个小区间,单点修改时,只更新包含这一元素的区间;求前n项和时,通过将区间进行组合,得到从 1 到 n 的区间,然后对所有用到的区间求和。
如何确定 的范围呢?树状数组巧妙利用了二进制,用 维护区间 。例如 维护 , 维护 。这样子划分后,查询前 n 项和时需要合并的区间数是少于 的。
那如何进行更新操作呢? 其实这是一个从叶子结点爬到根节点的过程。按照一开始的想法,当更新 时,只需要更新包含了 的 即可。以 为例,如图所示,包含了 的区间和包括 、、,因此只需要更新这三个即可。
爬树的过程是如何计算的? 对于当前所在位置 ,只需要不断加上 即可。例如初始 ,然后是 ,然后是 ......
至此我们就可以简单实现一个树状数组了。
class NumArray {
private int[] nums;
private int[] tree;
public NumArray(int[] nums) {
int n = nums.length;
this.nums = new int[n];
tree = new int[n + 1];
// 初始化树状数组
for(int i = 0; i < n; i++) {
update(i, nums[i]);
}
}
public void update(int index, int val) {
int delta = val - nums[index];
nums[index] = val;
// 从index一路往上更新
for(int i = index + 1; i < tree.length; i += i & -i) {
tree[i] += delta;
}
}
private int prefixSum(int index) {
int sum = 0;
// 从index一路往下求和
for(int i = index; i > 0; i -= i & -i) {
sum += tree[i];
}
return sum;
}
public int sumRange(int left, int right) {
return prefixSum(right + 1) - prefixSum(left);
}
}
/**
* 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);
*/
的作用
从正文中可以看出, 是树状数组实现的一个关键,那 是什么,为啥这么有用呢?
计算得到的是 的二进制表示中,最后一个 的位置,例如 , 等。
的计算公式很简单:。原理是在计算机中,负数用补码表示,即原码取反加一。因此 x 和 -x 仅从右往左第一个 ‘1’ 的位置是相同的。如下图: