树状数组的是一种动态维护数组区间和的数据结构。
1.树状数组和线段树有什么区别?
试想一个场景:
有一个长度为 n 的数组: [a0,a1,a2,a3,....a4] 现在有两种操作: 1.修改数组某个元素的值 2.获取数组某个区间的所有元素的和(即区间和)。
这种场景下,使用线段树是完全可以做到的。回顾:线段树的作用:对给定的一个长度为 n 的数组 nums[ 0 ..... n - 1]而言 ,线段树能够用来存储数组的某一连续子数组 nums[ left , right]的特征(可以是区间和、区间最大值、区间最小值)。
在本场景下,特征就是区间和。
例如,对于数组:[8,6,1,4,5,5,1,1,3,2,1,4,9,0,7,4],n = 16,线段树数组的树状结构是这样子的:
一维数组是这个样子的:
但如果用树状数组,则数组空间是这样样子的:树状数组的长度等于原数组长度 N
【结论】
也就是说,在解决【区间和】问题上,树状数组比线段树数组要节省至少一倍的空间!!!查找/修改操作的性能要优于线段树数组。
树状数组只能解决区间和问题,而线段树数组可以解决一系列的区间问题,比如区间和,区间最大值、最小值问题等。
2.树状数组的定义
定义:对于任意一个数组 a[1....n]
而言(放弃数组的第0个位置),我们可以构造它的树状数组 c[1.....n]
,
让数组 a 的前缀和 sum[a1 + a2 + ... + ax]
,即 prex[x]
,满足:
x 被二进制分解为:
$$
\Large x = 2^{i_1} + 2^{i_2}+2^{i_3}+...+2^{i_m} ,其中 i_1 > i_2 > i_3 ...
$$则
$$
\large prex[x] = c[2^{i_1}] + c[2^{i_1} + 2^{i_2}] + c[2^{i_1} + 2^{i_2} + 2^{i_3}] +... + c[x]
$$
例如, prex[13] = prex[(1101)] = perx[8 + 4 + 1] = c[8] + c[8+4] + c[8+4+1] = c[8] + c[12] + c[13]
有了这个定义,我们可以借助图来理解树状数组的各个元素之间的关系:(父节点值等于子节点值之和)
例如,c[9] = a[9], c[8] = c[4] + c[6] + c[7] + a[8], c[2] = c[1] + a[2],等等
3.树状数组单点修改
已知树状数组c
和原数组a
,当修改了原数组a的某个元素时,该如何调整数组c中的值以维持正确的树状数组呢?
【思考】
假设现需要将 a[x] (1 <= x <= n) 的值修改为 newValue,则等价于 往 a[x] 加上 t (t = newValue - a[x])
这时,我们需要做的,就是让该节点的直接父节点以及间接父节点的值都加上 t 即可。例如,往 a[5]上加了1,那就需要往 c[5]、c[6]、c[8] 上都加1.
【关键】
那我们又如何通过叶节点(即原数组节点),依次找到它的祖先节点呢?
$$
\large c[x] \quad 的父节点是 \quad c[x+ lowbit(x)]
$$
其中,lowbit(x) 的定义是:
$$
\large x = 2^{i_1} + 2^{i_2}+2^{i_3}+...+2^{i_m} ,其中 i_1 > i_2 > i_3 ... \\ \large lowbit(x) = 2^{i_m}
$$
例如,当x = 9,二进制分解: x = 8 + 1,则 C9的父节点是 c[9 + 1] = c[10]
例如,当 x = 12,二进制分解: x = 8 + 4,则c12的父节点是 c[12 + 4] = c[16]
【如何实现lowbit()】
* 如何求 lowBit(12)呢? 12的二进制表示为 0000 1100 (原码),如何得到最低位的为1的位是第几位呢? * 我们要的是从低位开始,第一个1保持不变,而其他位都变为 0 (正数的原码的符号位也是0) * 做法就是: 对原码按位取反后,加一,然后将结果与原码进行与运算。 * 例如, (0000 1100)取反 -> 11110011 加一 --> 11110100 * 11110100 & 00001100 -> 00000100 = 4 public int lowBit(int x){ return x & -x; //对原码取反+1,得到的正是该数的负数的二进制存储格式,所以这里直接写 -x }
4.树状数组的单点查询
已知树状数组 c[1.....n]
,如何计算 prex[x]
? 其中 1 <=x <= n
很简单,直接根据定义,将 x 进行二进制分解,得到数组c的一些元素的下标,然后相加这些元素即可。还是放定义中的例子:
prex[13] = prex[(1101)] = perx[8 + 4 + 1] = c[8] + c[8+4] + c[8+4+1] = c[8] + c[12] + c[13]
在编码上,我们可以用 lowbit来简化运算吗?是可以的!我们可以从x开始往前数,每次间隔lowbit的最小位。
当x = 13, c[13]肯定包括。 lowbit(13) = lowbit(1101) = 1, 所以 c[13-1] = c[12]也包括。 lowbit(12) = lowbit(1100) = 4,所以,c[12-4] = c[8]也包括。 lowbit(8) = lowbit(1000) = 8,c[8-8] = c[0],运算结束。
5.树状数组如何初始化?
如何根据原数组 a , 来初始化树状数组c呢?
【思路】
将初始化的过程等价于:对空数组 a 进行 n 次单点修改,同时更新树状数组。
6.练习与代码实现
307. 区域和检索 - 数组可修改https://leetcode.cn/problems/range-sum-query-mutable/
【代码】
/**
* 采用树状数组解法
*
*/
public class NumArray2 {
private int[] a;//原数组
private int[] c;//树状数组
private int n;
public NumArray2(int[] nums) {
n = nums.length;
a = new int[n+1];
c = new int[n+1];
for (int i = 0; i < n; i++){
add(i+1,nums[i]);
}
}
/**
* 单点修改
* @param x 要修改的位置, 1<=x <=n
* @param t 增量
*/
public void add(int x,int t){
a[x] += t;
int i = x;
while (i <= n){
c[i] += t;
i += lowBit(i);
}
}
/**
* 单点查询 1<=x <=n
* @param x
* @return 返回前缀和
*/
public int query(int x){
int sum = 0;
while (x > 0) {
sum += c[x];
x -= lowBit(x);
}
return sum;
}
public int lowBit(int x){
return x & -x; //对原码取反+1,得到的正是该数的负数的二进制存储格式,所以这里直接写 -x
}
public void update(int index, int val) {
int t = val - a[index + 1];
add(index + 1,t);
a[index + 1] = val;
}
public int sumRange(int left, int right) {
return query(right + 1) - query(left);
}
}