数据结构——树状数组详解

一.概念程序员

   树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的全部元素之和,可是每次只能修改一个元素的值;通过简单修改能够在log(n)的复杂度下进行范围修改,可是这时只能查询其中一个元素的值(若是加入多个辅助数组则能够实现区间修改与区间查询)。算法

  这种数据结构(算法)并无C++和Java的库支持,须要本身手动实现。在Competitive Programming的竞赛中被普遍的使用。树状数组和线段树很像,但能用树状数组解决的问题,基本上都能用线段树解决,而线段树能解决的树状数组不必定能解决。相比较而言,树状数组效率要高不少。——百度百科 
  首先明确一点,树状数组的本质仍是数组。
二.问题的引入
   先来看这样一道题目:
   

    要想查看本题,请点这里(有一些区别,本质相同)数组

       若是直接用数组进行模拟,修改的时间复杂度是O(1),查询是O(n)m次查询操做的时间复杂度就是O(mn),时间复杂度太高。数据结构

   树状数组就能够轻松处理这类问题,它是一个查询和修改的复杂度都为log(n)的数据结构。函数

    来看一下树状数组的样子:
图片出自水印。

 

    A表明原来的数组,C表明树状数组。为何树状数组要长成这样?优化

   明确一点:树状数组是对二进制的应用。spa

   咱们不妨把全部的数字都转换为二进制。来观察一下数字特征:code

    

    举几个例子:blog

   用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)

   


去超越本身不认同的人,去追赶本身理想的人。我想所谓的成长,就是不断的重复这些吧。
相关文章
相关标签/搜索