【算法与数据结构】—— 树状数组(基础) —— 酱懵静

树状数组基础


基本概念

何谓树状数组?
我想从其主要功能出发给出一个简单定义:能够快速完成单点修改以及区间求和功能的特殊数组。
在上面的话语需要注意一点,其依托的主要存储结构仍然是——数组。

要知道,在一些场合下总会出现数组求和的情形。如果数组的规模小还好,可一旦达到了某范围时,这时候求和就会变得十分缓慢(还有种情况就是求和过程也许还被包含在某个循环中,那么这时候,数据规模的增长甚至远超于线性递增),总之一句话,树状数组的存在,就是为了解决需要用到大量数据(次数)时,数组求和的一个需求。

当然,上面对其作用的总结仅仅是从最普遍的角度看去所得到的结论。要知道,算法与数据结构的结合,是会有无数种情况的。换言之,树状数组的作用不仅限于此。废话不多说,下面开始系统介绍树状数组的一些基础概念和核心操作。


首先要知道,树状数组是一个查询和修改复杂度都为log(n)的数据结构
其主要用于数组的单点修改以及区间求和
另外一个拥有类似功能的是线段树,两者的具体区别和联系如下:
1.在复杂度上同级, 但是树状数组的常数明显优于线段树, 其编程复杂度也远小于线段树。
2.树状数组的作用被线段树完全涵盖, 凡是可以使用树状数组解决的问题, 使用线段树一定可以解决, 但是线段树能够解决的问题树状数组未必能够解决。
3.树状数组的突出特点是其编程的极端简洁性, 使用lowbit技术可以在很短的几步操作中完成树状数组的核心操作,其代码效率远高于线段树。

上面提到了一个新名词:lowbit
其实lowbit(x)就是求x最低位的1


接下来从图像上直观地区别下普通数组和树状数组
对于一般的二叉树,我们是这样画的
在这里插入图片描述
把位置稍微移动一下,便是树状数组的画法
在这里插入图片描述
上图其实是求和之后的数组,原数组和求和数组的对照关系如下(其中A数组是原数组,C数组是求和后的数组,C[i]代表子树的叶子结点的权值之和):
在这里插入图片描述
如图可以知道:
C[1] = C[0001] = A[1];
C[2] = C[0010] = A[1]+A[2];
C[3] = C[0011] = A[3];
C[4] = C[0100] = A[1]+A[2]+A[3]+A[4];
C[5] = C[0101] = A[5];
C[6] = C[0110] = A[5]+A[6];
C[7] = C[0111] = A[7];
C[8] = C[1000] = A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];
现在将上图中的十进制数字转换为二进制再绘制,如下:
在这里插入图片描述
可以发现:C[ i ] = A[ i-2k+1 ] + A[ i-2k+2 ] +… + A[ i ](k为i的二进制中从最低位到高位连续零的长度),例:
i=8(1000)时,k=3,则C[1000] = A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];
i=6(0110)时,k=1,则C[110] = A[5]+A[6];
i=4(0100)时,k=2,则C[0100] = A[1]+A[2]+A[3]+A[4];
可以发现其满足上面列出的式子


上面给出的图文的阐述,其实是为了引出树状数组种最核心的一个函数——lowbit
根据上面的公式以及最开始给出的lowbit的主要功能(求x最低位的1),相信有的同学可能已经看出了,lowbit(i)便是上面的2^k
因为2k后面一定有k个0
比如说25 --> 100000
正好是i最低位的1加上后缀0所得的值
也就是说 lowbit ( x )=2k
这个函数的主要功能就是为了进行快速跳表,以此达到能很快的求出整个数组中和的目的。这一点,我相信你们从前面给出的图就能直观得看出来了。
比如说求前6项数组的和(这里假设p=6,参考上图走一遍代码你就懂lowbit了)

int sum(int p)
{
	int sum=0;
	while(p>0){
		sum+=C[p];
		p-=lowbit(p);
	}
}

首先函数进来是一个while循环
临时变量sum+=C[ 6 ] ,这时候sum就已经加了A[ 5 ]和A[ 6 ]的值了
接着p更新为p-=lowbit(6)(注意lowbit(6)=2),这时p=4
然后继续执行while循环中的内容
即sum+=C[ 4 ],到这里sum实际上就已经把A[ 1 ]、A[ 2 、]A[ 3 ]、A[ 4 ]的值都加进去了,也就是说完成了整个数组的求和了)
而接下来p更新为p-=lowbit(4)(注意lowbit(4)=4),这时p=0,退出循环,完成求和

可以看到,在一个长度为6的数组中,求和时,居然仅仅只求了两次和(和上图中的情形是一致的)
而在普通数组中,必然会执行6次的求和才会得到结果
因此,树状数组大大地加快了求和的进程

上面就是一个用于方便大家理解和认识树状数组的典型列子
下面给出lowbit函数的具体代码:

int  lowbit (int x) {  return x&(-x);  }

极致简短!!!现在我们来理解一下这行代码(基于函数本身的意义,从位(即根本)出发的阐述,有点打脑壳,如果想要学透彻那就看,否则建议跳过,反正做题你只要知道怎么用就行了)
首先要知道,对于一个数的负数就等于对这个数取反+1
以二进制数11010为例:11010的补码为00101,加1后为00110,两者相与(&)便是最低位的1
其实很好理解,补码和原码必然相反,所以原码有0的部位补码全是1,补码再+1之后由于进位那么最末尾的1和原码最右边的1一定是同一个位置(当遇到第一个1的时候补码此位为0,由于前面会进一位,所以此位会变为1)
所以我们只需要进行a&(-a)就可以取出最低位的1了


主要功能函数及其实现

单点更新(插值)

继续看开始给出的图
此时如果我们要更改A[1]
则有以下需要进行同步更新:

1(001) C[1]+=A[1]
lowbit(1)=001 1+lowbit(1)=2(010) C[2]+=A[1]
lowbit(2)=010 2+lowbit(2)=4(100) C[4]+=A[1]
lowbit(4)=100 4+lowbit(4)=8(1000) C[8]+=A[1]

换成代码就是:

void add(int pos,int value,int N)	//pos为更新的位置,value为更新后的数,n为数组长度
{
	while(pos<=N){
		C[pos]+=value;
		pos+=lowbit(pos);
	}
}


区间查询(求和)

下面利用C[i]数组,求A数组中前i项的和
举个例子 i=7;
sum[7]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7] ;
前i项和
C[4]=A[1]+A[2]+A[3]+A[4];
C[6]=A[5]+A[6];
C[7]=A[7];
可以推出: sum[7]=C[4]+C[6]+C[7];
序号写为二进制: sum[(111)]=C[(100)]+C[(110)]+C[(111)];

再举个例子 i=5
sum[5]=A[1]+A[2]+A[3]+A[4]+A[5] ;
前i项和
C[4]=A[1]+A[2]+A[3]+A[4];
C[5]=A[5];
可以推出: sum[5]=C[4]+C[5];
序号写为二进制: sum[(101)]=C[(100)]+C[(101)];

(细细观察二进制,树状数组追其根本就是二进制的应用)

下面给出区间查询的代码:

int sum(int pos)
{
	int ans=0;
	while(pos>0){
		ans+=C[i];
		pos-=lowbit(pos);
	}
	return ans;
}

结合代码分析:
对于i=7 进行演示
7(111)
ans+=C[7]

lowbit(7)=001
7-lowbit(7)=6(110)
ans+=C[6]

lowbit(6)=010
6-lowbit(6)=4(100)
ans+=C[4]

lowbit(4)=100
4-lowbit(4)=0(000)

对于i=5 进行演示
5(101)
ans+=C[5]

lowbit(5)=001
5-lowbit(5)=4(100)
ans+=C[4]

lowbit(4)=100
4-lowbit(4)=0(000)

发布了30 篇原创文章 · 获赞 67 · 访问量 3050

猜你喜欢

转载自blog.csdn.net/the_ZED/article/details/100159968
今日推荐