树状数组
一、树状数组的定义
树状数组(Binary Indexed Tree,简称 BIT)是一种高效的数组结构,专门用于处理单点更新 和前缀和查询操作,也可扩展处理区间查询、区间更新等问题。
它的核心优势是实现简单、空间复杂度低,相较于线段树,树状数组代码更简洁,常数更小,适合处理一维的前缀和相关问题。
资料:https://pan.quark.cn/s/43d906ddfa1b、https://pan.quark.cn/s/90ad8fba8347、https://pan.quark.cn/s/d9d72152d3cf
二、树状数组的核心原理
1. 二进制分解
树状数组的设计基于数字的二进制分解。对于一个正整数 x,其最低位的 1 对应的数值记为 lowbit(x),例如:
lowbit(6)(二进制 110)= 2(二进制 10)lowbit(8)(二进制 1000)= 8(二进制 1000)
lowbit(x) 的计算方法:lowbit(x) = x & -x(利用补码特性,负数的二进制是原码取反加 1)。
2. 数组结构
树状数组用一个数组 tree 存储信息,tree[i] 表示原数组中从 i - lowbit(i) + 1 到 i 这个区间的和(以区间和为例)。
- 叶子节点对应原数组的单个元素;
- 非叶子节点对应原数组的一段连续区间,区间长度由
lowbit(i)决定。
三、树状数组的核心操作
1. 单点更新(Point Update)
将原数组中第 idx 位置的元素增加 val(也可扩展为修改为指定值),需要更新树状数组中所有包含 idx 的区间节点。
- 操作逻辑:从
idx出发,不断加上lowbit(idx),直到超出数组长度,依次更新对应位置的tree值。
2. 前缀和查询(Prefix Sum Query)
查询原数组中从第 1 位到第 idx 位的前缀和(树状数组通常从 1 开始索引,避免处理 0 的 lowbit 问题)。
- 操作逻辑:从
idx出发,不断减去lowbit(idx),直到为 0,累加对应位置的tree值。
3. 区间查询(Range Sum Query)
查询原数组中 [l, r] 区间的和,可通过前缀和推导:query(r) - query(l - 1)。
四、时间复杂度
- 单点更新:
O(log n),每次更新最多遍历log n个节点; - 前缀和查询:
O(log n),每次查询最多遍历log n个节点; - 区间查询:基于两次前缀和查询,时间复杂度仍为
O(log n)。
五、树状数组的实现示例(以区间和为例)
python
class BinaryIndexedTree:
def __init__(self, data):
# 原数组(从1开始索引)
self.n = len(data)
self.tree = [0] * (self.n + 1)
# 初始化树状数组
for i in range(1, self.n + 1):
self.update(i, data[i - 1])
def lowbit(self, x):
# 计算最低位的1对应的数值
return x & -x
def update(self, idx, val):
# 单点更新:将idx位置增加val(idx从1开始)
while idx <= self.n:
self.tree[idx] += val
idx += self.lowbit(idx)
def query(self, idx):
# 查询前缀和:[1, idx](idx从1开始)
res = 0
while idx > 0:
res += self.tree[idx]
idx -= self.lowbit(idx)
return res
def range_query(self, l, r):
# 查询区间和:[l, r](l、r从1开始)
return self.query(r) - self.query(l - 1)
使用示例
python
# 原数组(索引0开始)
data = [1, 2, 3, 4, 5]
# 初始化树状数组(内部自动转为1开始索引)
bit = BinaryIndexedTree(data)
# 查询前缀和:[1, 3](对应原数组[0,2])
print(bit.query(3)) # 输出6(1+2+3)
# 查询区间和:[2,4](对应原数组[1,3])
print(bit.range_query(2, 4)) # 输出9(2+3+4)
# 单点更新:将原数组索引3(树状数组索引4)的值加2(原4→6)
bit.update(4, 2)
# 再次查询区间和:[2,4]
print(bit.range_query(2, 4)) # 输出11(2+3+6)
六、树状数组的扩展应用
1. 区间更新 + 单点查询
通过"差分思想"实现:
- 区间
[l, r]加val:执行update(l, val)和update(r+1, -val); - 单点查询
idx:查询前缀和query(idx)(差分的前缀和即为原数组值)。
2. 区间更新 + 区间查询
需要维护两个树状数组,结合差分公式推导,可实现区间加、区间和查询。
3. 其他统计场景
除了区间和,树状数组还可处理:
- 区间最大值/最小值(需特殊处理,不如线段树灵活);
- 逆序对统计(将数组离散化后,从后往前查询比当前数小的元素个数);
- 频率统计(统计某个数值范围内的元素个数)。
七、树状数组与线段树的对比
| 特性 | 树状数组 | 线段树 |
|---|---|---|
| 实现难度 | 简单,代码量少 | 较复杂,代码量多 |
| 空间复杂度 | O(n) | O(4n) |
| 单点更新 | O(log n),常数小 | O(log n),常数稍大 |
| 区间查询 | 仅擅长前缀和衍生的查询 | 支持任意区间统计(和、最值等) |
| 区间更新 | 需差分扩展,场景有限 | 结合懒标记,支持任意区间更新 |
八、适用场景
- 一维数组的单点更新 + 前缀和/区间和查询(树状数组的最优场景);
- 逆序对、频率统计等基于前缀和的统计问题;
- 数据量较大,对代码简洁性和常数效率要求高的场景。