LeetCode-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:
输入: |
提示:
1 <= nums.length <= 3 * 104
-100 <= nums[i] <= 100
0 <= index < nums.length
-100 <= val <= 100
0 <= left <= right < nums.length
- 调用
update
和sumRange
方法次数不大于3 * 104
树状数组
树状数组概述
树状数组或二元索引树(英语:Binary Indexed Tree),又以其发明者命名为Fenwick树,最早由Peter M. Fenwick于1994年以A New Data Structure for Cumulative Frequency Tables为题发表在SOFTWARE PRACTICE AND EXPERIENCE。其初衷是解决数据压缩里的累积频率(Cumulative Frequency)的计算问题,现多用于高效计算数列的前缀和, 区间和。它可以以
按照Peter M. Fenwick的说法,正如所有的整数都可以表示成2的幂和,我们也可以把一串序列表示成一系列子序列的和。采用这个想法,我们可将一个前缀和划分成多个子序列的和,划分的方法就是采用数的2进制划分。这样,子序列的个数是其二进制表示中1的个数。
比如14=11102,我们可以将其划分为三个子序列,长度分别为8=10002,4=1002,2=102。即要求[0, 14)的元素和,我们可以求出[0, 8)的和、[8, 12)的和、[12, 14)的和,再将其相加。
lowbit函数
定义一个lowbit
函数,返回参数的二进制中,最后一个1的位置所代表的数值。
例如,lowbit(34=00100010)
的返回值是2;而lowbit(12=1100)
返回4;lowbit(8=1000)
返回8。
代码实现如下:
int lowbit(int x) { |
简要分析一下这么写的原理。我们知道对于任意一个二进制数
树状数组实现
有了lowbit
函数,我们就可以方便地从数字中提取最低位的1了。
首先给出树状数组的概念:定义数组sum
为树状数组,nums
为原数据数组,则sum
满足以下性质:
sum[x]
存储的是nums
数组区间的值。
亦即,nums
数组从下标sum
数组中下标为nums
数组从下标
根据定义,不难知道树状数组的下标范围是nums
数组的下标范围是
上图展示了树状数组的结构和sum[i]
之间的包含关系。如sum[4]
所管辖的区间下包含了sum[1]
,sum[2]
和sum[3]
这三个子区间。
这里可以观察得出一些特点:
- 对于一个数
num[i]
,包含这个数的最小区间就是sum[i+1]
。 - 由子区间查询父区间的技巧:假设子区间的下标为
i
,则其父区间的下标为i + lowbit(i)
。
查询
假设我们想查询区间sum
数组中下标为sum
数组中下标为
实现查询算法的思路是,每次取对应sum[n]
的值,然后从
public int getSum(int n) { |
更新
更新时,我们首先计算出变化值delta
,然后将所有包含被更新下标的区间和加上变化值delta
。
更新的过程是,首先查询出包含该数的最小区间,然后依次向上更新父区间。
public void update(int index, int val) { |
新建
始终记住,树状数组中的下标
朴素建树算法如下,即按照树状数组的定义构建。
int[] sum, nums; |
除了标准建树方法外,还存在
观察上图,每一个节点的值是由所有与自己直接相连的儿子的值求和得到的。因此可以倒着考虑贡献,即每次确定完儿子的值后,用自己的值更新父亲。
// O(n)建树 |
其他应用
求逆序对数
逆序对数是一个数列中在它前面有比它大的个数。如4312的逆序对数是0+1+2+2=5。此问题可以用树状数组解决。