在计算机科学中,树状数组(Binary Indexed Tree)是一种精妙的数据结构,专为解决动态数组的前缀和和单点更新问题而设计。其原理基于二进制索引和位运算,以迅速、高效地完成这些操作而闻名。
树状数组的核心思想
树状数组的设计思想非常巧妙,基于二进制索引规则和位运算。它允许我们以较小的空间复杂度实现对动态数组的高效更新和查询操作。
实现一个前缀和例子
让我们来看一个实际的例子,演示如何使用树状数组解决前缀和问题。
假设我们有一个数组(该例子来自b站 五分钟丝滑动画讲解 | 树状数组)
我们想要快速计算任意范围内的前缀和,比如 [0, i]
区间的总和。我们可以先将该数组抽象成树状数组,数组中每个位置代表的前缀和如图。为什么图中有些数和有些前缀和不需要存储?答:因为只需维护以下这些数就可以求所有情况的前缀和了。
树状数组的操作:
-
更新(Update):
更新操作会改变树状数组中的一个特定位置,也会影响包含了这个数的位置的值。比如我要更新上图中序号(下标+1就是序号)为3(下标为2)的数1,而序号4,8,16的位置的数都包含了序号为1的数,因此都需要更新。而这个过程具有一定的规律。3+(3&-3)==4 ,4+(4&-4)==8,8+(8&-8)==16,更新直到序号超过数组个数便停止。n&-n这个操作叫lowbit,不懂可以去学习一下位操作的相关知识。 -
查询(Query):
查询操作用于计算数组的前缀和或特定区间的和。它也利用了树状数组索引的二进制特性,沿着树状数组向下移动,通过一系列的位运算操作,以有效地计算和返回所需的区间和。
如我要求前13个数的和,便需要将序号为8,12,13位置上的数相加便可,8位置的数是原数组前8个数的前缀和,12位置的数是原数组9~12这4个数的和,13位置的数是原数组13位置的数。规律是13&(13-1)==12 ,12&(12-1)==8 ,8&(8-1)==0,为0就结束。n&(n-1)相当于n-(n&-n)。
优点: 树状数组通过巧妙的索引规则和位运算,使得更新和查询的时间复杂度为 O(log n),其中 n 是数组的大小。
代码
// 更新树状数组的值
void update(vector<int> &bit, int i, int val) {
while (i < bit.size()) {
bit[i] += val;
i += i & -i;
}
}
// 计算前缀和
int query(const vector<int> &bit, int i) {
int total = 0;
while (i > 0) {
total += bit[i];
i = i & (i-1);
}
return total;
}
int main()
{
// 创建树状数组并计算前缀和
vector<int> nums = {3, 1, 5, 4, 2, 6};
int n = nums.size();
vector<int> BIT(n + 1, 0);//建立时多加一位方便下标与序号对应
for (int i = 0; i < n; ++i) {
update(BIT, i + 1, nums[i]);//传i+1,因为使用的是序号
}
// 查询前缀和
int prefixSum = query(BIT, 4);
// 输出前缀和
cout << "Prefix sum from index 0 to 3: " << prefixSum << endl; // 输出 13 (3 + 1 + 5 + 4 = 13)
return 0;
}
理解以上的题目后可以尝试做下面的leetcode题
leetcode链接
307. 区域和检索 - 数组可修改 - 力扣(LeetCode)
题解:
class NumArray {
public:
NumArray(vector<int>& nums) {
_n=nums.size();
BIT.resize(_n+1,0);
_nums.resize(_n,0);
//建立树状数组
for(int i=0;i<_n;i++)
update(i,nums[i]);
}
void update(int index, int val) {
int tmp=val-_nums[index];//计算要更新的差值
_nums[index]=val;//更新原数组
int n=index+1;//树状数组序号
//向上更新树状数组
while(n<=_n)
{
BIT[n]+=tmp;
n+=n&-n;
}
}
long long prevsum(int n)
{
long long ret=0;
while(n)
{
ret+=BIT[n];
n=n&(n-1);
}
return ret;
}
int sumRange(int left, int right) {
return prevsum(right+1)-prevsum(left);
}
vector<int> BIT;//树状数组
vector<int> _nums;//原数组
int _n;//数组大小
};