数据结构 树状数组

树状数组的作用是求区间和和做点更新。
和线段树相比,线段树和树状数组时间复杂度都是nlogn,但是在空间利用上,我们知道,线段树接近满二叉,需要的空间是4倍N,但是我们树状数组的空间和N相同,而且在代码上有明显的优势。
这里我们先看两幅图:
树状数组结构
树状数组就是上面的C[],表示的是下面这些数的和。
这里写图片描述

这里我们可以看到,所有的奇数位置都会表示它本身,如1,3,5,7……它们只包含自己
所有的2的倍数的位置都会除了本身都会包含它前面一个数,如2,4,6,8……除了他们本身,还包含了前一个数的和。
所有4的倍数都会再包含再之前的两个数,如4,8……不仅包含了本身和它前面一个数,还包含了再前面两个数的和。
所有8的倍数都会再包含再之前的4个数如8……
……
所有能被2^n整除的数都表示从该点前2^n个数的和。

通过上面这规律,如果我们要求1~n的和,只需要把它拆成二进制数的组合就行了。
比如求1~45的和,就等于c32+c40+c44+c45,这里因为c32表示1~32的和(能被32整除),c40表示33到40的和(能被8整除),c44表示41到44的和(能被4整除),c45表示45到45的和。
所以我们用树状数组的做法可以达到时间上是nlogn,空间上是n的效率。

那么我们如何实现呢?假如我们要寻找1~n的和。
首先我们得构建这个树状数组。
我们还是从左到右一步一步来。
假如n=8,我们的8个数分别是1 8 7 6 5 2 4 3
我们加第一个点,c1=1,那么反向推,c2会包含c1值,所以c2=c2+c1,c4会包含c2的值,所以c4=c4+c2,同理c8=c8+c4。所以我们第一次建完树是这样的。
这里写图片描述

第二次我们加点2,那么显然2,4,8处的点会被更新。
这里写图片描述

第三次我们加入点3,只有3,4,8会被更新。
这里写图片描述

第四次我们加点4,只有4和8要更新。
这里写图片描述

同理,加点5更新5,6,8;加点6更新6,8;加点7更新7,8;加点8更新8。

手动模拟建树我们会了,如何用代码实现呢,或许你有一大堆想法,但可能都不及下面这么简单:

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

void modify(int x,int n,int val)
{
    for(int i=x;i<=n;i=i+lowbit(i))
    {
        c[i]=c[i]+val;
    }
}

第二个函数大家应该都看得懂,相隔相应位置都加上该点的值嘛,但是这相隔位置是怎么个算的,逻辑运算符大家可能会很陌生,这里举个例子:
大家都知道5的二进制是0000 0101 ,那么-5的二进制是1111 1011,(二进制数取负数是先按位取反,再在最后一位+1),现在5&(-5)的二进制就是0000 0001,因为&操作符只有1&1=1,其他情况都是0。

这里大家可以发现,所有奇数的二进制末尾都是1,我们不管前面,按位取反后最后是0,再加1就末尾是1,其他位和原来相反,只有末尾和原来相同是1,所以得到的结果就是1。也就是我们更新了奇数点后接着要更新该点+1的位置。

假如我们的值是2的倍数(不包含2^2,2^3等),那么末尾是10,同理不管前面,取反变01,加1变10,前面和原来的数相反,末尾两位相同,所以得到的结果是2,也就是我们对2的倍数要继续更新它后面两个位置的点。比如2跳到4,6跳到8。

同理,对于4的倍数,末尾是100,取反+1后就还是100,结果是4,所以4的位置要更新完要跳到8。12的位置要跳到16。

同理,对于2^n的倍数,末尾是1000……(n个0),取反+1是本身,结果就是2^n,所以该点要跳到该点+2^n的点。
直到下一个要更新的点不在区间。

这样我们就完成了树状数组的建立。

对于查询,我们可以按建树的逆向思维来做,大家可以把上面45那个例子带入下面的代码手动模拟,求得的是1到x的和。

int getsum(int x)
{
    int sum=0;
    for(int i=x;i>=1;i=i-lowbit(i))
    {
        sum=sum+c[i];
    }
    return sum;
}

最后我们求区间x-y和的时候,只需要用1~y的和减去1~(x-1)的和就行了。

下面给个测试代码供大家细读。

#include <iostream>
#include"stdio.h"
const int N = 105;
int c[N];
using namespace std;

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

void modify(int x,int n,int val)
{
    for(int i=x;i<=n;i=i+lowbit(i))
    {
        c[i]=c[i]+val;
    }
}

int getsum(int x)
{
    int sum=0;
    for(int i=x;i>=1;i=i-lowbit(i))
    {
        sum=sum+c[i];
    }
    return sum;
}

int main()
{
    printf("请输入10个数\n");
    for(int i=1;i<=10;i++)
    {
        int t;
        scanf("%d",&t);
        modify(i,10,t);
    }
    printf("树状数组的值分别为:\n");
    for(int i=1;i<=10;i++)
    {
        printf("%d ",c[i]);
    }
    printf("\n从1到i的和分别是\n");
    for(int i=1;i<=10;i++)
    {
        printf("%d ",getsum(i));
    }
}

模拟运行结果如下:
这里写图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值