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


    一.概念

       树状数组(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代表树状数组。为什么树状数组要长成这样?

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

       我们不妨把所有的数字都转换为二进制。来观察一下数字特征:

        

        举几个例子:

       用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("
    ");
          }
          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("
    ");
          }
          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
    ",getsum(y) - getsum(x-1));
            }
        }
        return 0;
    }

        树状数组其他应用,之后会陆续补充。若对于其有新的理解,也会加入到其中。

       更新时间(2018.12.6)

       


    去超越自己不认同的人,去追赶自己理想的人。我想所谓的成长,就是不断的重复这些吧。
  • 相关阅读:
    接口测试 API测试
    接口测试 JMeter 开坑
    【测试笔记】集成测试 自顶向下 自底向上
    白盒测试 各类覆盖方法辨析
    eureka 管理界面打不开
    Spring Boot 2.0 Admin
    spring swagger2配置
    解决 Registered driver with driverClassName=oracle.jdbc.driver.OracleDriver was not found, trying direct instantiation.
    springboot+mybatis在插入空值时报错的问题
    Vue Cli 3代理配置
  • 原文地址:https://www.cnblogs.com/lizitong/p/10075286.html
Copyright © 2020-2023  润新知