一维树状数组
一维数组相信大家平时都是经常使用,对于一维数组而言,查询以及求和的时间复杂度分别为O(1) 和 O(n) 。
今天我们介绍一个新的数据结构——树状数组英文名称为Binary Index Tree,直译过来就是二进制索引树,我觉得二进制索引树更能说明其本质。树状数组的本质就是一种通过二进制位来维护一个序列前i和的数据结构。它的查询和求和的时间复杂度均为O(logn)。而且特别是当数组元素的值可以随时发生变化时,它的求和不需要像线性数组一样重新从头扫到尾更新求和而是只用更新改变的部分元素即可。其实树状数组通俗地说就是把一位线性数组模拟成树状,从而达到跳跃性的查询和求和。
序列C[]就是树状数组, 那么C[]如何求得?
C[1]=A[1];
C[2]=A[1]+A[2];
C[3]=A[3];
C[4]=A[1]+A[2]+A[3]+A[4];
C[5]=A[5];C[6]=A[5]+A[6];
C[7]=A[7];
C[8]= A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];
以上只是枚举了所有的情况,那么推广到一般情况,得到一个C[i]的抽象定义:因为A[]中的每个元素对应满二叉树的每个叶子,所以我们干脆把A[]中的每个元素当成叶子,那么:C[i] = C[i]的所有叶子的和。
现在不得不引出关于二进制的一个规律: 将十进制化成二进制,然后观察这些二进制数最右边1的位置:
1 --> 00000001
2 --> 00000010
3 --> 00000011
4 --> 00000100
5 --> 00000101
6 --> 00000110
7 --> 00000111
8 --> 00001000
1的位置其实从我画的满二叉树中就可以看出来。
但是这与C[]有什么关系呢?
接下来的这部分内容很重要:
在满二叉树中,以1结尾的那些结点(C[1],C[3],C[5],C[7]),其叶子数有1个,所以这些结点C[i]代表区间范围为1的元素和;以10结尾的那些结点(C[2],C[6]),其叶子数为2个,所以这些结点C[i]代表区间范围为2的元素和;以100结尾的那些结点(C[4]),其叶子数为4个,所以这些结点C[i]代表区间范围为4的元素和;以1000结尾的那些结点(C[8]),其叶子数为8个,所以这些结点C[i]代表区间范围为8的元素和。
扩展到一般情况:i的二进制中的从右往左数有连续的x个“0”,那么拥有2^x个叶子,为序列A[]中的第i-2^x+1到第i个元素的和。
终于,我们得到了一个C[i]的具体定义:C[i]=A[i-2^x+1]+…+A[i],其中x为i的二进制中的从右往左数有连续“0”的个数。
理解了C[i]后,前i个元素的和S[i]就很容易实现。
从C[i]的定义出发:C[i]=A[i-2^x+1]+…+A[i] (其中x为i的二进制中的从右往左数有连续“0”的个数)我们可以知道:C[i]是肯定包括A[i]的,那么:S[i]=C[i]+C[i-2^x]+… (其中x为i的二进制中的从右往左数有连续“0”的个数)也许上面这个公式太抽象了。
我们拿一个具体的实例来看:
S[7]=C[7]+C[6]+C[4] 因为C[7]=A[7],C[6]=A[6]+A[5],C[4]=A[4]+A[3]+A[2]+A[1],所以S[7]=C[7]+C[6]+C[4]S[7] = C[7] + C[6] + C[4]
但为什么S[7] 是 C[7] 和 C[6] 和C[4] 的和呢?S[7] 中必然有C[7], 但C[6] 和 C[4] 是如何得出的?
首先i=7 ( 7 的二进制为111 没有0 ,所以x = 0 )求得x=0根据公式 (S[i]=C[i]+C[i-2^x]+… 其中 i = i -2^x )
其次x = 0 i = i -2^x => i = 7 - 2^0 = 6
求得C[6] ( 6 的二进制为110 有一个0 所以x = 1)x = 1 i = i -2^x => 6- 2^1 = 4
最后一项为C[4],所以 S[7] = C[7] + C[6] + C[4]。
但是如何写程序判断二进制末尾有多少个零呢?
这里有个简单的结论: 2 ^ x = i & (-i) 至于为什么是这样,请先记住吧,这里要讲的太多了。
这个结论的代码如下:
int lowbit(int i)
{
return i&(-i);
}
下面是代码初始化或者更新:给某个节点i加上value
void change(int i, int value)
{
// n 为元素总个数
while(i<=n)
{
C[i]=C[i]+value;
i+=lowbit(i);
}
}
求前i项和:
- 首先令sum = 0.
- 判断,如果i>0 的话,就令sum+=C[i] 转向第三部,否则,终止算法返回sum值.
- i = i-lowbit(i), (将n的二进制表示的最后一个零删掉)回第二步
代码如下:
int Sum( int i )
{
int sum=0;
while( i>0 )
{
sum+=c[i];
i-=lowbit(i);
}
return sum;
}
更新 和 求和 一个 是 i+=lowbit(i) ,另一个是 i-=lowbit(i) 区别在于更新是从树叶向树根更新,也就是i 是从小到大 而求和是求 前 i 项的和,所以 i是最大,然后递减.