数据结构——树状数组详解
一.概念
树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。
要想查看本题,请点这里(有一些区别,本质相同)
如果直接用数组进行模拟,修改的时间复杂度是O(1),查询是O(n),m次查询操作的时间复杂度就是O(mn),时间复杂度过高。
树状数组就可以轻松处理这类问题,它是一个查询和修改的复杂度都为log(n)的数据结构。
A代表原来的数组,C代表树状数组。为什么树状数组要长成这样?
明确一点:树状数组是对二进制的应用。
我们不妨把所有的数字都转换为二进制。来观察一下数字特征:
举几个例子:
用C来代表树状数组,用A来代表原数组:
C(2) = A(1)+ A(2) 对应二进制 C(0010) = A(0010) + A(0001)
C(4) = A(4)+A(2)+A(3) 对应二进制C(0100) = A(0100)+A(0010)+ A(0011)
C(8) = A(8)+A(4)+A(6)+A(7)对应二进制C(1000) = A(1000)+A(0100)+A(0110)+A(0111)
......
不难发现,对于任意的C[i]写成二进制的形式,都等于原来的数组A[i]的值本身,加上把i转换成二进制后,把首位变成0,并且开始逐位向后把0变成1,相加。
例如: 1000(二进制)向后逐位变化,即1000--> 0100-->0110>0111
......
构建出这样的树状数组后:
查询A1到A8的和,只需要返回C8
查询A1到A7,只需返回C7+C6+C4
查询A1到A6,只需要返回C6+C4
以此类推。
这样便优化了询问的时间复杂度。
那么说的这么好听,如何做到修改A的值时,顺便更新C的所有关于A的值呢?(例如,修改A[1]的时候更新C1,C2,C4,C8)
三.补码、lowbit函数
1.补码
首先说一说补码。
——百度百科。
补码是计算机表示符号数的一种方式,概念内容太杂乱,对于我们树状数组没有太大的用处,只需要了解两个事情:
·正整数的补码和原码(原来的二进制的代码)相同。
·负整数的补码将其正整数的原码所有为取反(0变1,1变0),之后加一。
2.lowbit函数
lowbit函数便是帮助我们找到二进制表达式中最低位1所对应的值。
比如,6的二进制是110,所以lowbit(6)=2。
lowbit的实现方式一共两种:个人推荐下面这种写法:
int lowbit(int x)
{
return x&(-x);
}
&符号意思为按位与,把两个数的二进制一位一位比较,都为1,结果则为1,否则就是0。
这个函数什么意思?将一个数x的原码和补码按位与?返回的就是最后一位1?
的确是这样。
举个例子(example):
6 = 0110(二进制)
它的补码为0010。按位与之后得到的确实就是最后一位1。
正是因为补码在取反后+1,才有了lowbit函数的产生。
您不妨打开计算器,切换到程序员模式,试上几组,或许会有新的领悟。
毕竟“纸上得来终觉浅,绝知此事要躬行”。
......
3.lowbit查询与更新的应用:
首先先来运行一段代码:
#include<stdio.h>
int main()
{
int i,j;
for(i=1;i<=8;i++)
{
printf("%d:",i);
for(j=i;j<=8;j+=j&-j)
printf("%d ",j);
printf("\n");
}
return 0;
}
Output:
1: 1 2 4 8
2: 2 4 8
3: 3 4 8
4: 4 8
5: 5 6 8
6: 6 8
7: 7 8
8: 8
图片数字对比来看更新A1,需要更新C1,C2,C4,C8。
更新A2,需要更新C2,C4,C8。更新A3,需要更新C3,C4,C8.....
正好与我们代码运行出来的结果一致。这就为我们向上更新提供了条件。
再来看一下查询。如果查询A1到A8和,只需要返回C8,查询A1到A7,只需要返回C7+C6+C4
再来看下面这组代码:
#include<stdio.h>
int main()
{
int i,j;
for(i=1;i<=8;i++)
{
printf("%d:",i);
for(j=i;j;j-=j&-j)
printf("%d ",j);
printf("\n");
}
return 0;
}
Output:
1: 1
2: 2
3: 3 2
4: 4
5: 5 4
6: 6 4
7: 7 6 4
8: 8
这段代码只是将之前的i+=lowbit(i)修改为了i-=lowbit(i)
再对比之前原图,查询A1到A8,只需要返回C8,查询A1到A7,只需要返回C7,C6,C4与我们代码运行的效果一致,这就为我们向下查询提供了条件。
四.树状数组应用
再回头看之前引出树状数组的题目,这时候就可以有一定的思路了。
树状数组主要的函数分为两个,即更新函数和查询函数。
void fix(int x)
{
int i;
for(i=x;i<=n;i+=i&-i) //向上更新
e[i]++; //一维树状数组e
}
//注意fix()的形参值x<=0时死循环。
int getsum(int x)
{
int ret=0,i; //返回值为ret,初值为0
for(i=x;i;i-=i&-i)//向下查询
ret+=e[i];
return ret;
}
//注意getsum()的形参值x<0时e[ ]数组越界。
void fix(int x)
{
int i;
for(i=x;i;i-=i&-i) //向下更新
e[i]++;
}
int getsum(int x)
{
int ret=0,i;
for(i=x;i<=n;i+=i&-i)//向上查询
ret+=e[i];
return ret;
}
树状数组可以有两个方向,1.向下更新,向上查询 2.向上更新,向下查询。本题用的是向上更新,向下查询。
注意:树状数组更新时是增加量,初始时候更新量就是它本身。
所以开始时利用更新函数,将每一个点更新,之后只要输出就行了。
代码:
#include<stdio.h>
int a[100005];
int c[100005];
int n;
int lowbit(int x)
{
return x&(-x);
}
void add(int x,int ad)
{
for(int i = x;i<=n;i+=lowbit(i))
{
c[i]+=ad;
}
}
int getsum(int x)
{
int ans = 0;
for(int i = x;i>0;i-=lowbit(i))
{
ans+=c[i];
}
return ans;
}
int main()
{
scanf("%d",&n);
for(int i = 1;i<=n;i++)
{
scanf("%d",&a[i]);
add(i,a[i]);
}
int m;
scanf("%d",&m);
for(int i = 1;i<=m;i++)
{
char s[2];
int x,y;
scanf("%s%d%d",s,&x,&y);
if(s[0]=='C')
{
add(x,y-a[x]);
a[x] = y;
}else
{
printf("%d\n",getsum(y) - getsum(x-1));
}
}
return 0;
}
树状数组其他应用,之后会陆续补充。若对于其有新的理解,也会加入到其中。
更新时间(2018.12.6)
去超越自己不认同的人,去追赶自己理想的人。我想所谓的成长,就是不断的重复这些吧。