参考资料:
[1、Beyond the Void - 树状数组][1]
[2、夜深人静写算法(三) - 树状数组][2]
树状数组(Binary Index Tree)亦即二进制索引树,一种通过二进制位来维护一个序列前i和的数据结构。
1、树 or 数组 ?
名曰树状数组,那么究竟它是树还是数组呢?数组在物理空间上是连续的,而树是通过父子关系关联起来的,而树状数组正是这两种关系的结合,首先在存储空间上它是以数组的形式存储的,即下标连续;其次,对于两个数组下标x,y(x < y),如果x + 2^k = y (k等于x的二进制表示中末尾0的个数),那么定义(y, x)为一组树上的父子关系,其中y为父结点,x为子结点。
如图二-1-1,其中A为普通数组,C为树状数组(C在物理空间上和A一样都是连续存储的)。树状数组的第4个元素C4的父结点为C8 (4的二进制表示为"100",所以k=2,那么4 + 2^2 = 8),C6和C7同理。C2和C3的父结点为C4,同样也是可以用上面的关系得出的,那么从定义出发,奇数下标一定是叶子结点。
2、结点含义
然后我们来看树状数组上的结点Ci具体表示什么,这时候就需要利用树的递归性质了。我们定义Ci的值为它的所有子结点的值 和 Ai 的总和,之前提到当i为奇数时Ci一定为叶子结点,所以有Ci = Ai ( i为奇数 )。从图中可以得出:
C1 = A1
C2 = C1 + A2 = A1 + A2
C3 = A3
C4 = C2 + C3 + A4 = A1 + A2 + A3 + A4
C5 = A5
C6 = C5 + A6 = A5 + A6
C7 = A7
C8 = C4 + C6 + C7 + A8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
建议直接看C8,因为它最具代表性。 我们从中可以发现,其实Ci还有一种更加普适的定义,它表示的其实是一段原数组A的连续区间和。根据定义,右区间是很明显的,一定是i,即Ci表示的区间的最后一个元素一定是Ai,那么接下来就是要求Ci表示的第一个元素是什么。从图上可以很容易的清楚,其实就是顺着Ci的最左儿子一直找直到找到叶子结点,那个叶子结点就是Ci表示区间的第一个元素。 更加具体的,如果i的二进制表示为 ABCDE1000,那么它最左边的儿子就是 ABCDE0100,这一步是通过结点父子关系的定义进行逆推得到,并且这条路径可以表示如下:
ABCDE1000 => ABCDE0100 => ABCDE0010 => ABCDE0001
这时候,ABCDE0001已经是叶子结点了,所以它就是Ci能够表示的第一个元素的下标,那么我们发现,如果用k来表示i的二进制末尾0的个数,Ci能够表示的A数组的区间的元素个数为2^k,又因为区间和的最后一个数一定是Ai,所以有如下公式:
Ci = sum{ A[j] | i - 2^k + 1 <= j <= i } (帮助理解:将j的两个端点相减+1 等于2^k)
3、求和操作
明白了Ci的含义后,我们需要通过它来求sum{ A[j] | 1 <= j <= i },也就是之前提到的sum(i)函数。为了简化问题,用一个函数lowbit(i)来表示2^k (k等于i的二进制表示中末尾0的个数)。那么:
sum(i) = sum{ A[j] | 1 <= j <= i }
= A[1] + A[2] + ... + A[i]
= A[1] + A[2] + A[i-2^k] + A[i-2^k+1] + ... + A[i]
= A[1] + A[2] + A[i-2^k] + C[i]
= sum(i - 2^k) + C[i]
= sum( i - lowbit(i) ) + C[i]
由于C[i]已知,所以sum(i)可以通过递归求解,递归出口为当i = 0时,返回0。sum(i)函数的函数主体只需要一行代码:
int sum(int x)
{
return x ? C[x]+ sum( x - lowbit(x)):0;
}
观察 i - lowbit(i),其实就是将i的二进制表示的最后一个1去掉,最多只有log(i)个1,所以求sum(n)的最坏时间复杂度为O(logn)。由于递归的时候常数开销比较大,所以一般写成迭代的形式更好。写成迭代代码如下:
int sum(int x)
{
int s =0;
for(int i = x; i ; i -= lowbit(i))
{
s += c[i][j];
}
return s;
}
4、更新操作
更新操作就是之前提到的add(i, 1) 和 add(i, -1),更加具体得,可以推广到add(i, v),表示的其实就是 A[i] = A[i] + v。但是我们不能在原数组A上操作,而是要像求和操作一样,在树状数组C上进行操作。
那么其实就是求在Ai改变的时候会影响哪些Ci,看图二-1-1的树形结构就一目了然了,Ai的改变只会影响Ci及其祖先结点,即A5的改变影响的是C5、C6、C8;而A1的改变影响的是C1、C2、C4、C8。
也就是每次add(i, v),我们只要更新Ci以及它的祖先结点,之前已经讨论过两个结点父子关系是如何建立的,所以给定一个x,一定能够在最多log(n) (这里的n是之前提到的值域) 次内更新完所有x的祖先结点,add(i, v)的主体代码(去除边界判断)也只有一行代码:
void add(int x,int v)
{
if(x <= n)
{
C[x]+= v, add( x + lowbit(i), v );
}
}
和求和操作类似,递归的时候常数开销比较大,所以一般写成迭代的形式更好。写成迭代形式的代码如下:
void add(int x,int v)
{
for(int i = x; i <= n; i += lowbit(i))
{
C[i] += v;
}
}
##5、lowbit函数O(1)实现
>上文提到的两个函数sum(x)和add(x, v)都是用递归实现的,并且都用到了一个函数叫lowbit(x),表示的是2k,其中k为x的二进制表示末尾0的个数,那么最简单的实现办法就是通过位运算的右移,循环判断最后一位是0还是1,从而统计末尾0的个数,一旦发现1后统计完毕,计数器保存的值就是k,当然这样的做法总的复杂度为O( logn ),一个32位的整数最多可能进行31次判断(这里讨论整数的情况,所以符号位不算)。
>这里介绍一种O(1)的方法计算2k的方法。
>来看一段补码小知识:
>清楚补码的表示的可以跳过这一段,计算机中的符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。这里只讨论整数补码的情况,在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理。整数补码的表示分两种:
> 正数:正数的补码即其二进制表示;
> 例如一个8位二进制的整数+5,它的补码就是 00000101 (标红的是符号位,0表示"正",1表示“负”)
> 负数:负数的补码即将其整数对应的二进制表示所有位取反(包括符号位)后+1;
> 例如一个8位二进制的整数-5,它的二进制表示是00000101,取反后为11111010,再+1就是11111011,这就是它的补码了。
> 下面的等式可以帮助理解补码在计算机中是如何工作的
> +5 + (-5) = 00000101 + 11111011 = 1 00000000 (溢出了!!!) = 0
> 这里的加法没有将数值位和符号位分开,而是统一作为二进制位进行计算,由于表示的是8进制的整数,所以多出的那个最高位的1会直接舍去,使得结果变成了0,而实际的十进制计算结果也是0,正确。
>补码复习完毕,那么来看下下面这个表达式的含义:__<code> x & (-x) (其中 x >= 0)</code>
>首先进行&运算,我们需要将x和-x都转化成补码,然后再看&之后会发生什么,我们假设 x 的二进制表示的末尾是连续的 k 个 0,令x的二进制表示为 X0X1X2…Xn-2Xn-1, 则 {Xi = 0 | n-k <= i < n}, 这里的X0表示符号位。
>x的补码就是由三部分组成: (0)(X1X2…Xn-k-1)(k个0) 其中Xn-k-1为1,因为末尾是k个0,如果它为0,那就变成k+1个0了。
>-x的补码也是由三部分组成: (1)(Y1Y2…Yn-k-1)(k个0) 其中Yn-k-1为1,其它的Xi和Yi相加为1,想想补码是怎么计算的就明白了。
>那么 x & (-x) 也就显而易见了,由两部分组成 (1)(k个0),表示成十进制为 2k 啦。
>由于&的优先级低于-,所以代码可以这样写:
>```
int lowbit(int x)
{
return x & -x;
}
>```
##小结:
>__树状数组下标要从1开始,因为在修改值的时候,树状数组对于下标0不能正确处理<code> i += i & -i</code>得出来的结果一直是0,造成死循环__
>对于维护的序列A,定义C[i]=A[j+1]+...+A[i],其中j为i的二进制表示中把最右边的1换成0的值。j的值可以通过lowbit求出,即i-lowbit(i)。
lowbit(a)为2^(a的二进制表示末尾0的个数)。可以用下面式子求出
>```
lowbit(a)=a&(~a+1)
>```
或者根据补码的性质简化为
>```
lowbit(a)=a&(-a)
>```
修改方式如下
>```
void modify(int p,int delta)
{
while (p<=N)
{
C[p]+=delta;
p+=lowbit(p);
}
}
>```
求前缀和如下
>```
int sum(int p)
{
int rs=0;
while (p)
{
rs+=C[p];
p-=lowbit(p);
}
return rs;
}
>```
[1]:https://www.byvoid.com/blog/binary-index-tree
[2]:http://www.cppblog.com/menjitianya/archive/2015/11/02/212171.html