您的位置:首页 > 理论基础 > 数据结构算法

数据结构之树状数组

2011-05-15 21:34 357 查看

1、概述

树状数组(binary indexed tree),是一种设计新颖的数组结构,它能够高效地获取数组中连续n个数的和。概括说,树状数组通常用于解决以下问题:数组{a}中的元素可能不断地被修改,怎样才能快速地获取连续几个数的和?

2、树状数组基本操作

传统数组(共n个元素)的元素修改和连续元素求和的复杂度分别为O(1)和O(n)。树状数组通过将线性结构转换成伪树状结构(线性结构只能逐个扫描元素,而树状结构可以实现跳跃式扫描),使得修改和求和复杂度均为O(lgn),大大提高了整体效率。

给定序列(数列)A,我们设一个数组C满足

C[i] = A[i–2^k+ 1] + … + A[i]------【C[i] = a[i-lowbit(i)+1] + …+ a[i]i-lowbit(i)+1 是什么?就是i把最右边的1去掉,然后再加1】

其中,k为i在二进制下末尾0的个数,i从1开始算!

则我们称C为树状数组。

下面的问题是,给定i,如何求2^k?

答案很简单:2k=i&(i^(i-1)) ,也就是i&(-i)

下面进行解释:


----这里有错,应该为(2)10
这里用到低位技术:

  低位技术即Lowbit技术。相信熟悉树状数组(BIT)的朋友应该并不陌生。

  我们对于一个非0数x,现在提取出其最低位的1。这里我提三种不同的写法。

  Lowbit(x)=x&(x^(x-1))

  Lowbit(x)=x&~(x-1)

  Lowbit(x)=x&-x

  注意:这里我们求出的是x中最后一个1表示的数,而非其位置。

  可以发现,这三种低位函数的写法可谓大同小异——均涉及到了x&和x-1(其实 –x 可以认为是和 ~(x-1) 等价的,这里利用了负数的存储原理)。

  x-1的性质在于:其将一个数最后一个1变成了0,并把原来这个1之后0的位置均变成了1。低位技术正是利用了这个性质。

数组C的具体含义如下图所示:



当我们修改A[i]的值时,可以从C[i]往根节点一路上溯,调整这条路上的所有C[]即可,这个操作的复杂度在最坏情况下就是树的高度即O(logn)。另外,对于求数列的前n项和,只需找到n以前的所有最大子树,把其根节点的C加起来即可。不难发现,这些子树的数目是n在二进制时1的个数,或者说是把n展开成2的幂方和时的项数,因此,求和操作的复杂度也是O(logn)。(如求前4项的和,(4)10==(0100)2,所以找出一个就可以,它是c[4];其它也一样).

树状数组能快速求任意区间的和:A[i] + A[i+1] + … + A[j],设sum(k) = A[1]+A[2]+…+A[k],则A[i] + A[i+1] + … + A[j] = sum( j )-sum( i-1 )。

下面给出树状数组的C语言实现:

//*******************求低位*******************//
int lowbit(int t)//lowbit(x) 实际上就是x的二进制表示形式留下最右边的1,其他位都变成0
{
return t & ( -t );
}

//*******************求前n项和*******************//
int sum(int x)
{
int tot = 0;
while(x > 0)
{
tot += c[x];
x -= lowbit(x);//x - lowbit( x )是什么样子?就是x的二进制最右边的1变为0,例如x=7,则sum=c[7]+c[6]+c[4],具体可以见上图并且写出7的二进制0111,可以看出接下来下标为6,4
}
return tot;
}

//*******************更新*******************//
void update(int pos, int num)
{
while(pos <= n)  //pos + lowbit(pos) 必须小于等于a 的元素个数n
{
c[pos] += num;
pos += lowbit(pos);
}
}


3、扩展——二维树状数组

一维树状数组很容易扩展到二维,二维树状数组如下所示:

C[x][y] = sum(A[i][j])

其中,x-lowbit[x]+1 <= i<=x且y-lowbit[y]+1 <= j <=y

4、应用

(1) 一维树状数组:

参见:http://hi.baidu.com/lilu03555/blog/item/4118f04429739580b3b7dc74.html

(2) 二维树状数组:

一个由数字构成的大矩阵,能进行两种操作

1) 对矩阵里的某个数加上一个整数(可正可负)

2) 查询某个子矩阵里所有数字的和

要求对每次查询,输出结果

(3)求第k小元素

#define N (1<<20)
int c
;
//复杂度log(n)
int find_k(int k){
int ans = 0,cnt = 0;
for(int i=log(double(N-1))/log(2.0);i>=0;i--){//把树状数组的求和反向模拟作二分逼近
ans += (1<<i);
if(ans>=N || cnt+c[ans]>=k)ans -= (1<<i);
else cnt += c[ans];
}
return ans+1;
}


5、总结

树状数组最初是在设计压缩算法时发现的(见参考资料1),现在也会经常用语维护子序列和。它与线段树(具体见:数据结构之线段树)比较在思想上类似,比线段树节省空间且编程复杂度低,但使用范围比线段树小(如查询每个区间最小值问题)。

所以,树状数组适合单个元素经常修改而且还反复要求部分的区间的和的情况。上述问题虽然也可以用线段树解决,但是用树状数组来做,编程效率和程序运行效率都更高。如果每次要修改的不是单个元素,而是一个区间,那就不能用树状数组了(效率过低)。

6、参考资料

(1) Binary Indexed Trees:

http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=binaryIndexedTrees

(2) 吴豪文章《树状数组》:

http://www.java3z.com/cwbwebhome/article/article19/zip/treearray.zip

(3) 郭炜文章《线段树和树状数组》:强烈建议看一下!!!

http://poj.org/summerschool/1_interval_tree.pdf
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: