前言
在前一部分的学习中,我们了解了什么是树状数组,它的原理是什么,我们对其做了一定的知识储备
今天我们进一步了解,树状数组如何实现,他又是如何维护数组,等一系列操作的
树状数组的性质
我们用数组C【】来进行维护,数组C可以看作如图所示的树形结构
图中最下面一行是N个叶节点(N=16),代表数值a【1 ~ N】该结构满足以下性质:
1.每个内部节点 c[x] 保存以它为根的子树的所有叶节点的和
2.每个节点内部 c[x] 的子节点个数等于 lowbit(x)的位数
3.除树根外,每个根内部节点c[x]的父节点是 c[ x+lowbit(x) ]
4.树的深度为O(logN)
维护操作
1.查询前缀和
根据上述性质,求节点x的前缀和的代码实现如下
inline int lowbit(int x){
return (x) & (-x);
}
//define lowbit(x) ((x) & (-x))
inline int ask(int x){
int tot=0;
for(; x; x-=lowbit(x)){
tot+=c[x];
}
return tot;
}
根据前缀和性质,当我们要查询区间 [ L , R ] 时
只需要求 ask ( R ) - ask ( L-1 )
inline int query(int l,int r){
return ask(r)-ask(l-1);
}
2.单点修改
当给序列 a 中的一个数增加 num 时,我们需要同时完成对点的修改与对前缀和的维护
我们知道当给 a [ x ] 增加一个数num时,只有其本身以及其所有祖先节点保存的区间和需要增加,而任意一个节点的祖先至多只有 logN 个 我们只需逐一更新即可,实现如下:
inline void update(int x,int num){
for(; x<=n; x+=lowbit(x)){
c[x]+=num;
}
}
上述代码的时间复杂度为O( logN )
3.初始化树状数组
在执行所有操作之前,我们需要对树状数组初始化,即建立一个树状数组对原数组进行维护;
一般的初始化方法为,直接建立一个空数组 c 然后依次更新每个位置的前缀和值,代码如下:
inline void build(){
for(int i=1;i<=n;i++){
update(i,a[i]);
}
}
更高效的方法是: 从小到大依次考虑每个节点 x ,借助 lowbit 运算扫描它的子节点并求和
采用这种方法时,其树的每条边只会被遍历一次,时间复杂度为
O ( ∑ k = 1 l o g N k ∗ N / 2 k ) = O ( N ) O( \sum_{k=1}^{logN} { k* N/2^k})= O(N) O(k=1∑logNk∗N/2k)=O(N)
总结与思考
关于树状数组的基础学习到这里就结束了,对于这一数据结构在处理逆序对等问题上效果优异。在关于数据结构的学习之后一定要多加刷题来巩固知识。
下一部分我们将继续学习一种更加通用的数据结构—线段树