树状数组是一种高效的数据结构,主要用来解决以下两种常见问题:
- 单点更新:修改数组中某个位置的值(比如把第 3 个元素加 2)。
- 前缀和查询:计算数组从开头到某个位置的和(比如第 1 到第 5 个元素的和)。
在普通数组中,这两个操作的时间复杂度通常是“此消彼长”的:
- 如果直接用原始数组,单点更新是 O(1),但前缀和查询需要遍历数组,时间复杂度是 O(n)。
- 如果用一个前缀和数组(提前计算每段的和),前缀和查询是 O(1),但单点更新需要修改后面所有的前缀和,时间复杂度是 O(n)。
树状数组的厉害之处在于,它能让这两种操作都达到 O(log n) 的时间复杂度,既高效又平衡,非常适合动态维护数组数据的场景。
树状数组的原理
树状数组的核心是通过一个辅助数组(我们叫它 tree),把原始数组的元素组织成一种“树形结构”。但它并不是真正的树,而是利用数字的二进制特性来管理区间和。
Lowbit 函数
要理解树状数组,首先要搞清楚一个关键函数:lowbit(x)。它返回的是一个数的二进制表示中“最低位的 1”所对应的值。怎么理解呢?我们看几个例子:
- 对于数字 6,二进制是 110,最低位的 1 在第 2 位(从右往左数),对应值是 2(二进制 10),所以 lowbit(6) = 2。
- 对于数字 4,二进制是 100,最低位的 1 在第 3 位,对应值是 4(二进制 100),所以 lowbit(4) = 4。
- 对于数字 3,二进制是 11,最低位的 1 在第 1 位,对应值是 1(二进制 1),所以 lowbit(3) = 1。
计算方法:在计算机中,lowbit(x) 可以用 x & -x 来快速计算。
Lowbit 的作用:它决定了树状数组中每个节点负责的区间范围,是更新和查询时的“步长”。
树状数组的结构
假设我们有一个原始数组 arr = [a1, a2, a3, ..., an](为了方便,下标从 1 开始,而不是 0),树状数组用一个数组 tree[1..n] 来存储信息。关键点在于:
- tree[i] 负责存储原始数组中一段区间的和,具体是从位置 i - lowbit(i) + 1 到 i 的和。
- 例如:
- tree[1] 负责 [1, 1](因为 lowbit(1) = 1,区间长度是 1)。
- tree[2] 负责 [1, 2](因为 lowbit(2) = 2,区间长度是 2)。
- tree[4] 负责 [1, 4](因为 lowbit(4) = 4,区间长度是 4)。
这种结构让每个 tree[i] 管理一个长度为 lowbit(i) 的区间,而这些区间通过二进制的跳跃关系巧妙地覆盖整个数组。
树状数组的操作
单点更新(Update)
假如我们想把原始数组中位置 i 的值增加一个数 delta(比如把 arr[2] 加 2),需要更新树状数组中所有包含 i 的节点。具体步骤是:
- 从位置 i 开始。
- 把 tree[i] 增加 delta。
- 计算下一步:i = i + lowbit(i),跳到下一个需要更新的节点。
- 重复步骤 2 和 3,直到 i 超过数组长度 n。
为什么这样更新?
- 每次 i += lowbit(i) 会跳到包含当前区间的一个更大的区间。
- 比如更新 i = 1,会影响:
- tree[1](负责 [1, 1]),
- 然后 i = 1 + lowbit(1) = 2,更新 tree[2](负责 [1, 2]),
- 再 i = 2 + lowbit(2) = 4,更新 tree[4](负责 [1, 4]),依此类推。
这样,所有的“上级节点”都会感知到这个变化。
前缀和查询(Query)
假如我们想计算从 1 到位置 i 的前缀和,步骤是:
- 初始化一个变量 sum = 0。
- 从位置 i 开始。
- 把 tree[i] 加到 sum 上。
- 计算下一步:i = i - lowbit(i),跳到前一个区间。
- 重复步骤 3 和 4,直到 i 变成 0。
为什么这样查询?
- 每次 i -= lowbit(i) 会跳到覆盖更小区间的节点,最终把所有覆盖 [1, i] 的区间和加起来。
- 比如查询 i = 3 的前缀和:
- tree[3](负责 [3, 3]),
- i = 3 - lowbit(3) = 2,tree[2](负责 [1, 2]),
- 加起来就是 [1, 3] 的和。
Java 实现
下面是一个完整的 Java 代码示例,展示了如何构建树状数组、执行单点更新和前缀和查询。
class FenwickTree {
private int[] tree; // 树状数组
private int n; // 数组大小
// 构造函数:初始化树状数组
public FenwickTree(int size) {
n = size;
tree = new int[n + 1]; // 下标从 1 开始
}
// 单点更新:在位置 i 增加 delta
public void update(int i, int delta) {
while (i <= n) {
tree[i] += delta;
i += lowbit(i); // 跳转到下一个需要更新的位置
}
}
// 前缀和查询:计算从 1 到 i 的和
public int query(int i) {
int sum = 0;
while (i > 0) {
sum += tree[i];
i -= lowbit(i); // 跳转到前一个区间
}
return sum;
}
// 计算 lowbit(x)
private int lowbit(int x) {
return x & -x;
}
}
// 测试代码
public class Main {
public static void main(String[] args) {
int[] arr = {0, 1, 3, 5, 7, 9, 11}; // 原始数组,下标从 1 开始
FenwickTree fenwick = new FenwickTree(arr.length - 1);
// 构建树状数组
for (int i = 1; i < arr.length; i++) {
fenwick.update(i, arr[i]);
}
// 查询前缀和 [1, 3]
System.out.println(fenwick.query(3)); // 输出 9 (1+3+5)
// 更新 arr[2] += 2,使 arr[2] 从 3 变为 5
fenwick.update(2, 2);
// 再次查询前缀和 [1, 3]
System.out.println(fenwick.query(3)); // 输出 11 (1+5+5)
}
}
具体例子
假设我们有一个原始数组 arr = [1, 3, 5, 7, 9, 11](下标从 1 到 6)。
构建树状数组
我们根据规则计算 tree 数组:
- tree[1] = arr[1] = 1(负责 [1, 1])。
- tree[2] = arr[1] + arr[2] = 1 + 3 = 4(负责 [1, 2])。
- tree[3] = arr[3] = 5(负责 [3, 3])。
- tree[4] = arr[1] + arr[2] + arr[3] + arr[4] = 1 + 3 + 5 + 7 = 16(负责 [1, 4])。
- tree[5] = arr[5] = 9(负责 [5, 5])。
- tree[6] = arr[5] + arr[6] = 9 + 11 = 20(负责 [5, 6])。
初始状态:
arr = [1, 3, 5, 7, 9, 11]
tree = [1, 4, 5, 16, 9, 20]
查询前缀和
- 查询 [1, 3] 的和:
- 从 i = 3 开始:
- tree[3] = 5(负责 [3, 3]),
- i = 3 - lowbit(3) = 3 - 1 = 2,tree[2] = 4(负责 [1, 2]),
- i = 2 - lowbit(2) = 2 - 2 = 0,停止。
- sum = 5 + 4 = 9,正确(1 + 3 + 5 = 9)。
- 从 i = 3 开始:
- 查询 [1, 6] 的和:
- 从 i = 6 开始:
- tree[6] = 20(负责 [5, 6]),
- i = 6 - lowbit(6) = 6 - 2 = 4,tree[4] = 16(负责 [1, 4]),
- i = 4 - lowbit(4) = 4 - 4 = 0,停止。
- sum = 20 + 16 = 36,正确(1 + 3 + 5 + 7 + 9 + 11 = 36)。
- 从 i = 6 开始:
更新元素
- 把 arr[2] 增加 2(从 3 变成 5):
- 从 i = 2 开始:
- tree[2] = 4 + 2 = 6,
- i = 2 + lowbit(2) = 2 + 2 = 4,tree[4] = 16 + 2 = 18,
- i = 4 + lowbit(4) = 4 + 4 = 8,超出范围,停止。
- 更新后:
- 从 i = 2 开始:
arr = [1, 5, 5, 7, 9, 11]
tree = [1, 6, 5, 18, 9, 20]
再次查询 [1, 3]:
- i = 3:
- tree[3] = 5,
- i = 3 - 1 = 2,tree[2] = 6,
- i = 2 - 2 = 0,停止。
- sum = 5 + 6 = 11,正确(1 + 5 + 5 = 11)。
图解:直观理解结构
以 n = 8 为例,树状数组的结构可以用下面这张简化的图表示(括号里是负责的区间):
tree[8] (1-8)
/ \
tree[4] (1-4) tree[6] (5-6)
/ \ / \
tree[2] (1-2) tree[3] (3-3) tree[5] (5-5) tree[7] (7-7)
/ \
tree[1] (1-1) tree[2] (1-2)
- 每个节点负责一段区间,区间长度由 lowbit 决定。
- 查询和更新时,通过 lowbit 的加减操作在树中跳跃。
为什么高效?
树状数组的更新和查询都是 O(log n),因为:
- 每次跳跃(i += lowbit(i) 或 i -= lowbit(i)),相当于把 i 的二进制最低位清零或改变,最多跳 log n 次。
- 比如 n = 8 时,最多跳 3 次(因为 8 = 2^3)。
相比之下,线段树也能做到 O(log n),但树状数组更简单、占内存更少,非常适合前缀和相关问题。

被折叠的 条评论
为什么被折叠?



