0%

树状数组

简介

树状数组又叫二叉索引树(binary indexed tree),是一种用数组来表示“树”形状的数据结构,这里的“树”并不是真正意义上的树,是一个抽象意义上的“树”,其部分节点并不真实存在,只是为了方便理解而将其用“树”来表示。树状数组常用于求解动态数组(数组中的值经常发生变化)中的区间和。

算法

一般情况下,画二叉树都会将父节点放在左右子节点的中间,对于树状数组,将父节点位置调整到右子节点的方向上,如下图所示。

binary-indexed-tree-1

然后将树中白色背景的节点删除(算法过程中并不需要这部分节点),将蓝色背景的节点统一放到一个水平线上,就可以得到一个数组,如下图所示

binary-indexed-tree-2

基于此,就可以用一个数组来表示存储一颗“树”,并用这颗“树”来执行相关业务计算,比如区间和。

但是,对于一个原始数组,怎么将其表示成树状数组呢。首先,要引入一个 lowbit 的概念,lowbit(n) 表示正整数 n 的二进制数的最低位 1 的位置对应数(整数二进制 32 位数里,只在这个位置有数 1,其他位置均为 0)。比如 6 的二进制数是 110,则 lowbit(6) = 25 的二进制数为 101,则 lowbit(5) = 18 的二进制数为 1000, 则 lowbit(8) = 16。关于 lowbit 有个快速的计算方法: lowbit(n) = n & (-n)

知道了 lowbit 的含义,现在来构建树状数组。假设用 C 数组来表示构建的树状数组,C[n] 表示 n 节点或左右子树节点之和(如果存在左右子树的话),对于叶子节点,会自动对应到原始数组中。从下图中可以看出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
C[1] = nums[1]
C[2] = C[1] + nums[2] = nums[1] + nums[2]
C[3] = nums[3]
C[4] = C[2] + C[3] + nums[4] = nums[1] + nums[2] + nums[3] + nums[4]
C[5] = nums[5]
C[6] = C[5] + nums[6] = nums[5] + nums[6]
C[7] = nums[7]
C[8] = C[4] + C[6] + C[7] + nums[8] = nums[1] + nums[2] + nums[3] + nums[4] + nums[5] + nums[6] + nums[7] + nums[8]
C[9] = nums[9]
C[10] = C[9] + nums[10] = nums[9] + nums[10]
C[11] = nums[11]
C[12] = C[10] + nums[11] + nums[12] = nums[9] + nums[10] + nums[11] + nums[12]
C[13] = nums[13]
C[14] = C[13] + nums[14] = nums[13] + nums[14]
C[15] = nums[15]
C[16] = C[8] + C[12] + C[14] + C[15] + nums[16] = nums[1] + nums[2] + nums[3] + nums[4] + nums[5] + nums[6] + nums[7] + nums[8] + nums[9] + nums[10] + nums[11] + nums[12] + nums[13] + nums[14] + nums[15] + nums[16]

binary-indexed-tree-3

从上述的数组等式中,可以看出,树状数组中的元素包含了原始数组中的部分元素。当初始数组的某个元素发生变化时,树状数组中的部分元素需要跟着改变(树状数组的元素构成里包含了初始数组的这个元素)。比如改变 nums[1] 时,C[1]C[2]C[4]C[8]C[16]都会发生改变,改变 nums[7] 时, C[7]C[8]C[16] 会跟着改变,改变 nums[16] 时,只有 C[16] 会发生变化。

那当某个值发生变化时,怎么知道有哪些值会受影响呢。这里有个递归公式可以判断:当树状数组中 i 的位置发生变化时, i + lowbit(i) 的位置也会发生变化。所以可以得出以下的代码

1
2
3
4
5
6
7
8
9
10
// nums 是初始数组,sums 是树状数组
// 此函数功能:改变初始数组 index 的值为 val,然后更新树状数组中的元素
public void update(int index, int val) {
int increaseNum = val - nums[index];
nums[index] = val;
// 重要的是递推过程元素下标的变化:i += lowBit(i)
for (int i = index; i < sums.length; i += lowBit(i)) {
sums[i] += increaseNum;
}
}

上述的代码虽然是对数组的更新操作,但是同样可以用在构建树状数组的过程中。因为构建树状数组的过程可以看做是将一个初始数组元素都是 0 的数组更新成目标数组的元素的过程。因此,构建树状数组的代码可以是以下操作

1
2
3
4
5
6
// 此处假设 nums[0] 是无意义的,真正有意义的数从 nums[1] 开始的
// nums 是初始数组,sums 是树状数组
int[] sums = new int[nums.length];
for (int i = 1; i < nums.length; i++) {
update(i, nums[i]);
}

说完了更新操作,接下来看查询操作。由于树状数组中的元素包含了部分初始数组的元素,因此,当查询初始数组的前缀和或区间和时,可以利用树状数组来快速求解。当求初始数组某个位置的前缀和时,可以利用递推公式在树状数组里进行计算,递归的规律刚好是跟更新操作相反。具体代码如下

1
2
3
4
5
6
7
8
9
10
11
// sums 是根据初始数组构建出来的树状数组,sums[0] 无意义,真正有意义的数从 sums[1] 开始
// 此函数功能,求解初始数组中 index 位置的前缀和
private int querySum(int index) {
int sum = 0;
// 重要的是递推过程元素下标的变化:i -= lowBit(i)
for (int i = index; i > 0; i -= lowBit(i)) {
sum += sums[i];
}

return sum;
}

例子

算法大部分都有模板代码,树状数组也一样,以下给出一个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class NumArray {
int[] nums;

int[] sums;

// 初始化,构建树状数组,树状数组 sums[0] 留空(无意义),从 sums[1] 开始,sums[i] 对应 nums[i - 1]
public NumArray(int[] nums) {
this.nums = new int[nums.length];
// sums[0] 无意义,从 sums[1] 开始,sums[i] 对应 nums[i - 1]
sums = new int[nums.length + 1];
for (int i = 0; i < nums.length; i++) {
update(i, nums[i]);
}
}

// 将初始数组的 index 位置更新为 val,同时更新树状数组的元素
public void update(int index, int val) {
int increaseNum = val - nums[index];
nums[index] = val;
// 重要的是递推过程元素下标的变化:i += lowBit(i)
for (int i = index + 1; i < sums.length; i += lowBit(i)) {
sums[i] += increaseNum;
}
}

// 计算初始数组 index 位置的前缀和
private int querySum(int index) {
int sum = 0;
// 重要的是递推过程元素下标的变化:i -= lowBit(i)
for (int i = index; i > 0; i -= lowBit(i)) {
sum += sums[i];
}

return sum;
}

// 计算正整数 n 对应的二进制数的最低位 1 所在位置对应的数
private int lowBit(int n) {
return n & (-n);
}
}