数据结构之树状数组(Binary Indexed Tree, BIT)
·
树状数组(Binary Indexed Tree, BIT)详解
1. 引言
树状数组(Binary Indexed Tree, BIT)是一种高效的数据结构,由Peter M. Fenwick在1994年提出。它主要用于解决前缀和查询和点更新问题,能够在O(log n)的时间复杂度内完成这两种操作。相比线段树,树状数组实现更简单,代码量更少,但在功能上稍显局限。
2. 基本概念
2.1 核心思想
树状数组的核心思想是将数组元素按照二进制位进行分组,每个节点存储特定范围内元素的和。这种结构使得前缀和查询和点更新操作都能在O(log n)时间内完成。
2.2 数据结构
树状数组通常使用一个辅助数组tree[]来存储,其中tree[i]存储原数组中从i - lowbit(i) + 1到i的元素和。
lowbit(i) = i & (-i):获取i的二进制表示中最低位的1及其后面的0组成的数
3. 核心操作
3.1 前缀和查询
要查询数组前i个元素的和,可以使用以下方法:
def query(i):
result = 0
while i > 0:
result += tree[i]
i -= i & -i # 移除最低位的1
return result
3.2 点更新
要更新数组中第i个元素的值,可以使用以下方法:
def update(i, delta):
while i <= n: # n为数组大小
tree[i] += delta
i += i & -i # 跳到下一个需要更新的节点
4. 实现示例
4.1 Python实现
class BinaryIndexedTree:
def __init__(self, nums):
self.n = len(nums)
self.tree = [0] * (self.n + 1)
for i in range(self.n):
self._update(i + 1, nums[i])
def _update(self, i, delta):
while i <= self.n:
self.tree[i] += delta
i += i & -i
def update(self, index, val):
delta = val - self.sumRange(index, index)
self._update(index + 1, delta)
def query(self, i):
result = 0
while i > 0:
result += self.tree[i]
i -= i & -i
return result
def sumRange(self, left, right):
return self.query(right + 1) - self.query(left)
4.2 C++实现
class BinaryIndexedTree {
private:
vector<int> tree;
int n;
public:
BinaryIndexedTree(vector<int>& nums) {
n = nums.size();
tree.resize(n + 1, 0);
for (int i = 0; i < n; ++i) {
update(i, nums[i]);
}
}
void update(int index, int val) {
int delta = val - sumRange(index, index);
for (int i = index + 1; i <= n; i += i & -i) {
tree[i] += delta;
}
}
int query(int i) {
int result = 0;
for (; i > 0; i -= i & -i) {
result += tree[i];
}
return result;
}
int sumRange(int left, int right) {
return query(right + 1) - query(left);
}
};
5. 时间复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 构造 | O(n log n) | O(n) |
| 点更新 | O(log n) | - |
| 前缀和查询 | O(log n) | - |
6. 应用场景
6.1 典型应用
- 频率统计:统计数组中前i个元素的出现次数
- 区间和查询:快速计算任意子数组的和
- 逆序对统计:在排序算法中统计逆序对数量
- 动态前缀和:支持动态更新的前缀和计算
7. 与其他数据结构的比较
7.1 与线段树的比较
| 特性 | 树状数组 | 线段树 |
|---|---|---|
| 实现复杂度 | 简单 | 复杂 |
| 代码量 | 少 | 多 |
| 功能 | 仅前缀和 | 区间查询、区间更新 |
| 空间复杂度 | O(n) | O(n) |
| 构造时间 | O(n log n) | O(n) |
7.2 与前缀和数组的比较
| 特性 | 前缀和数组 | 树状数组 |
|---|---|---|
| 更新时间 | O(n) | O(log n) |
| 查询时间 | O(1) | O(log n) |
| 适用场景 | 静态数据 | 动态数据 |
8. 扩展应用
8.1 二维树状数组
对于二维前缀和问题,可以使用二维树状数组:
class BinaryIndexedTree:
def __init__(self, matrix):
if not matrix or not matrix[0]:
return
self.m, self.n = len(matrix), len(matrix[0])
self.tree = [[0] * (self.n + 1) for _ in range(self.m + 1)]
for i in range(self.m):
for j in range(self.n):
self._update(i + 1, j + 1, matrix[i][j])
def _update(self, row, col, delta):
i = row
while i <= self.m:
j = col
while j <= self.n:
self.tree[i][j] += delta
j += j & -j
i += i & -i
def query(self, row, col):
result = 0
i = row
while i > 0:
j = col
while j > 0:
result += self.tree[i][j]
j -= j & -j
i -= i & -i
return result
8.2 离散化应用
对于值域较大的数据,可以先进行离散化处理,再使用树状数组:
def discretization(arr):
unique = sorted(set(arr))
return {v: i+1 for i, v in enumerate(unique)}
9. 性能优化技巧
9.1 循环展开
在关键路径上可以适当展开循环以减少分支预测失败:
int query(int i) {
int result = tree[i];
i -= i & -i;
if (i > 0) {
result += tree[i];
i -= i & -i;
if (i > 0) {
result += tree[i];
i -= i & -i;
// 可以继续展开...
}
}
return result;
}
9.2 内存布局优化
确保数据在内存中连续存储,以提高缓存命中率。
10. 常见误区
- 索引从1开始:树状数组的实现通常从1开始索引,而不是0
- 边界处理:注意数组越界问题,特别是更新和查询时的边界条件
- 负数处理:
lowbit操作对负数同样有效,但需要注意符号位 - 大数处理:对于大数运算,注意整数溢出问题
11. 实际案例分析
11.1 逆序对统计
def count_inversions(arr):
# 离散化
sorted_unique = sorted(set(arr))
rank = {v: i+1 for i, v in enumerate(sorted_unique)}
bit = BinaryIndexedTree([0] * len(sorted_unique))
inversions = 0
for i in range(len(arr)-1, -1, -1):
r = rank[arr[i]]
inversions += bit.query(r-1)
bit.update(r, 1)
return inversions
11.2 动态频率统计
class FrequencyTracker:
def __init__(self, nums):
self.bit = BinaryIndexedTree(nums)
self.freq = {}
def update(self, index, val):
old_val = self.freq.get(index, 0)
self.bit.update(index, val - old_val)
self.freq[index] = val
def query(self, left, right):
return self.bit.sumRange(left, right)
12. 总结
树状数组是一种优雅而高效的数据结构,特别适用于需要频繁进行前缀和查询和点更新操作的场景。它的实现简单,性能优秀,是算法竞赛和实际应用中的重要工具。虽然功能上不如线段树全面,但在特定场景下具有不可替代的优势。
通过理解其底层原理和掌握正确的实现技巧,开发者可以有效地利用树状数组解决各种复杂问题。
更多推荐
所有评论(0)