树状数组(Binary Indexed Tree)

前面几篇文章我们分享的都是关于区间求和问题的几种解决方案,同时也介绍了线段树这样的数据结构,我们从中可以体会到合理解决方案带来的便利,对于大部分区间问题,线段树都有其绝对的优势,今天这篇文章,我们就来欣赏由线段树变形的另外一个数据结构--树状数组,树状数组通常也用于解决区间求和、单点更新的问题,而且效率比线段树高一些(树状数组区间求和和单点更新的时间复杂度均为o(log n)),相对而言,线段树的应用范围可能更广泛一些。但不得不承认,树状数组确实也是一种优雅高效的结构。接下来,我们就一起来揭开它的神秘面纱。

第一点,树状数组的结构和线段树类似,但是比线段树的节点少,第二点,树状数组的每个节点中存储的也是对应范围的元素和,同时与线段树一样,树状数组也是采用数组存储结构,第三点,树状数组与线段树的下标定义规则不同。如下图所示,这是线段树的存储图:

我们从上篇文章(文章链接:线段树第二弹(区间更新))中可以发现,线段树的下标编码规则是由上而下、从左至右依次编码,在树状数组中,不再采用这样的编码方式,具体如何编码稍后将会解释,现在我们先来观察一下线段树的特征。

我们假设,此线段树中每个节点存储的是相应的区间范围内所有元素的和。这时我们可以发现,对于每个右孩子节点的值,我们总能通过其父亲节点值减去左兄弟节点值来计算,这就意味着,即使没有右节点,也丝毫不影响我们求解对应区间的区间和,那我们何不节省空间,但这样又引发了另外一个问题,在去掉所有的右节点之后,之前的下标编码方式肯定是用不了了,这时候我们就需要一套新的下标编码方式,既能节省这部分空间,又不会将原来的问题复杂化,最好能将问题进一步简化,这就是树状数组的产生背景。至于新的一套编码方式一路走来经历怎样的探索过程,不是我们今天要说明的重点,在此我就不作过多的解释,现在我就直接抛出树状数组最后确定版的下标编码方式,使用这个方式的原因当然是:它简单啊、好用啊、优雅啊,何以见得?等看完这篇文章,你大概就能体会到它的魅力了。

如下图所示为树状数组的逻辑结构,其中每个节点中存储的依然是对应区间的元素和:

这是树状数组的逻辑结构,其中方块中的数字表示对应区间的下标范围,红色字体表示节点的下标,图中的蓝色粗线条将每个节点和其对应的下标连接(线条这么粗,大概不会有人看不清楚了吧)。乍一看觉得节点下标的编码方式似乎有点无理取闹,同一层级的两个节点竟然下标不相连,这是什么逻辑?每当我们感觉走投无路的时候,也就是我们需要重新审视手里掌握的所有线索的时候,只有不放过任何一个细微的线索,才能找到破解之法。不妨我们就将所有能观察到的线索一一列出。

上面我们介绍的其实是树状数组的逻辑结构,它的物理存储就是一个一维的数组。我们将上图的特征制表如下:

观察上表,我们可以得出如下结论:

一、节点下标为 i 时,节点中对应的最后一个元素下标为 i

二、节点下标对应的二进制数末尾有 k 个 0 ,节点中对应的元素个数为 2 ^ k

三、节点中对应的元素下标是连续的

四、树状数组的节点个数和原数据元素个数相等

以上便是树状数组的主要基本特征,知道了这些特征之后,我们可以发现,要改变原数据数组中的一个元素值,在树状数组中最多需要更改 o(log n)个节点值,因此单点更新的时间复杂度为 o(log n)。单点更新的具体实现怎么做,在文章末尾会向大家展示,现在先继续讨论接下来的问题。

这时候我们会发现,刚才所列出来的所有特征,似乎没什么用得上的,就像在生活中我们手里掌握的零碎的知识、技能、人脉等看起来是一片散沙,我们不知道什么时候才会用得到,甚至穷尽一生也不可能全都用得到,但是一旦有机会用,我们才能真正意识到那些是多么的重要,其中的联系是多么紧密。与其说学习算法是在学一门技术,不如说是在学习一门艺术,因为在此期间接触到的很多方法都可以从生活中找到影子。所以我们暂时不要灰心,继续研究,也许更深入些,这些琐碎的特征就会变得有用。

树状数组方便处理的其实是“前 i 个元素和”这种问题,

以上图为例:

前 1 个元素和为

sum[1] = sum[0001] = tree[1]= tree[0001]

前 2 个元素和为

sum[2] = sum[0010] = tree[2]=tree[0010]

前 3 个元素和为

sum[3] = sum[0011] = tree[2]+tree[3]= tree[0011] + tree[0010]

前 4 个元素和为

sum[4] =sum[0100] = tree[4]= tree[0100]

前 5 个元素和为

sum[5] = sum[0101]= tree[4]+tree[5]=tree[0101]+tree[0100]

前 6 个元素和为

sum[6] = sum[0110] = tree[4]+tree[6]= tree[0110] + tree[0100]

前 7 个元素和为

sum[7]=sum[0111]=tree[7]+tree[6]+tree[4]=tree[0111]+tree[0110]+tree[0100]

前 8 个元素和为

sum[8] = sum[1000] = tree[8]= tree[1000]

红色部分是下标的二进制表示形式,我觉得到目前为止,我们可能真的是走投无路,才无所不用其极,连下标也不放过。仔细观察这些下标,似乎还是有一定的规律可循的,现在我们就挑一个表达式最长,能说明问题的来研究一下

sum[7]=sum[0111]=tree[7]+tree[6]+tree[4]=tree[0111]+tree[0110]+tree[0100]

由上述表达式可以发现,前7个元素和 sum[0111] 的加数包括 tree[0111], 在此基础上,每次将下标从右向左数第一位 1 抹去作为下一个加数的下标, 直到数字变为 0 结束,0111 抹去最后一位 1 得到 0110,0110 抹去最后一位 1 得到 0100,0100 抹去最后一位 1 得到 0000 结束运算。这似乎勉强可以算作一个规律吧,经过验证发现,以上所有的表达式均符合这个规律。在此我可以告诉大家,经过无数的高手验证,这个规律确实存在,所以我们可以大胆的使用。

但是,在我们用话语描述的时候,可以说抹掉最后一位 1 ,在实际的实现中,我们就需要用规范的语言来表达,要想达到抹掉最后一位 1 的效果,就需要减去一个数 x ,将 0111 抹掉最后一位 1 得到 0110 时,x = 0001,将0110抹掉最后一位 1 得到 0100 时,x = 0010 ,将 0100 抹掉最后一位 1 时,x = 0100 。也就意味着,原数值需要抹掉哪一位,那么 x 的哪一位就为 1 ,其余各位均为 0 。如何求原数值最后一位 1 是哪一位呢?我们可以发现原数值末位有 k 个 0 时,x = 2 ^ k,现在,我们前面列出来的特征就有联系了。目前我们的任务就是求 k 的值,当然对于人来说,一眼看出一个数末尾有几个0简直易如反掌,但对于计算机,似乎没那么容易,这时候如果将原数值的二进制数看作整体,似乎不合理,我们需要将其各位分离,这就用到了位运算。对于当前的问题,有一个求解技巧可以分享给大家,我们都知道在计算机内部两数的运算用的是补码实现的,对于正数来说,补码和原码形式是一样的,但对于负数来说,补码便是将原码按位取反后在末位加 1 ,这就导致一个负数绝对值的补码和这个负数的补码在形式上满足:以从右向左数第一个非零位为界,在左侧,负数绝对值的补码和负数的补码各位均不相同,在右侧,负数绝对值的补码和负数的补码各位均为0,将两数做 and 运算得到的数字刚好就是我们需要求的 x ,我们知道 负数的绝对值和负数本身互为相反数,同时也说明一个正数的补码与其相反数的补码做 and 运算 得到的数字就是 x 。

验证实例如下:

1 &(-1)补码运算 : 0001 & 1111 = 0001

2 &(-2)补码运算 : 0010 & 1110 = 0010

3 &(-3)补码运算 : 0011 & 1101 = 0001

4 &(-4)补码运算 : 0100 & 1100 = 0100

5 &(-5)补码运算 : 0101 & 1011 = 0001

6 &(-6)补码运算 : 0110 & 1010 = 0010

7 &(-7)补码运算 : 0111 & 1001 = 0001

8 &(-8)补码运算 : 1000 & 1000 = 1000

由此,我们得到每次的减数 x =i & ( -i )

至此,求 前 n 项和的规律已经找到,我们的任务就是将这个规律用规范的语句描述并尝试着用代码实现。

求和具体实现描述如下:

假设 当前求前 i 项 之和

第一步:判断 i > 0 是否成立。如果成立进行下一步,否则退出循环

第二步:sum = sum + tree[ i ] , x= i &(-i)

第三步:i = i - x ,回到第一步

知道了前 i 项和的求解方法之后,要求解区间和相对来说就容易多了,例如求解区间为 i ~ j ,显然,我们就可以得知

sum[ i~j ] = sum [ j ] - sum [ i ]  。

解决了前 i 项和的问题之后,现在我们再回过头来分析单点更新的问题,还是刚才的图:

假设我们现在要修改的元素在原数据中下标为 3 ,那么在树状数组中,我们需要修改的节点下标分别为 3 、4、8

还是按照刚才的分析方法,在原数据中待修改元素下标为 3 = 0011

在树状数组中,需要修改的节点下标为 3 = 0011、4 =0100、8 =1000,

观察发现,0011 +0001 = 0100 ,0100 +0100 = 1000 ,发现规律了吗?x 值依然是刚才的求法,现在是每次给当前的下标值加 x 就得到下一个加数的下标值,直到下标值大于元素的总个数停止。具体的实例不过多赘述,大家可以自己私下验证。

单点更新的具体描述如下:(假设更新的规则是给 下标为 i 的元素加 y )

假设待更新元素下标为 i  ,元素总个数为 n

第一步:判断 i <= n 是否成立,成立则进行下一步,不成立结束循环

第二步:tree[ i ] = tree[ i ] + y , x = i & ( -i ) ,进行下一步

第三步:i = i + x ,跳回第一步

以上就是今天内容的理论部分,下面为大家奉上核心代码实现部分,希望今天的分享能让大家有收获。代码如下:

除了并查集,这应该是见过的最精简最优雅最高效的代码了。

还没有关注公众号的朋友可以长按下图识别图中二维码关注我。

老规矩,打开网页http://paste.ubuntu.com/25548013/查看网页版代码。

树状数组(Binary Indexed Tree,BIT)的更多相关文章

  1. 树状数组(Binary Indexed Tree) 总结

    1.“树状数组”数据结构的一种应用 对含有n个元素的数组(a[1],...,a[k],...,a[n]): (1)求出第i个到第j个元素的和,sum=a[i]+...+a[j]. 进行j-i+1次加法 ...

  2. 树状数组 Binary Indexed Tree/Fenwick Tree

    2018-03-25 17:29:29 树状数组是一个比较小众的数据结构,主要应用领域是快速的对mutable array进行区间求和. 对于一般的一维情况下的区间和问题,一般有以下两种解法: 1)D ...

  3. 树状数组(Binary Indexed Tree(BIT))

    先不说别的,这个博客为我学习树状数组提供了很大帮助,奉上传送门 http://blog.csdn.net/int64ago/article/details/7429868 然后就说几个常用的操作 in ...

  4. 树状数组(Binary Index Tree)

    一维BIT(单点更新,区间求和): Problem - 1166 #include <iostream> #include <algorithm> #include <c ...

  5. 树状数组,Fenwick Tree

    Fenwick Tree, (also known as Binary Indexed Tree,二叉索引树), is a high-performance data structure to cal ...

  6. 树形DP+DFS序+树状数组 HDOJ 5293 Tree chain problem(树链问题)

    题目链接 题意: 有n个点的一棵树.其中树上有m条已知的链,每条链有一个权值.从中选出任意个不相交的链使得链的权值和最大. 思路: 树形DP.设dp[i]表示i的子树下的最优权值和,sum[i]表示不 ...

  7. HDU 3436--Queue-jumpers (树状数组 or Splay Tree)

    树状数组这个真心想了好久,还是没想出来 %%% www.cppblog.com/Yuan/archive/2010/08/18/123871.html 树状数组求前缀和大于等于k的最大值,第一次看到这 ...

  8. 树状数组(fenwick tree)

    树状数组又称芬威克树,概念上是树状,实际上是使用数组实现的,表现为一种隐式数据结构,balabala...详情请见:https://en.wikipedia.org/wiki/Fenwick_tree ...

  9. NYOJ 108 士兵杀敌1(树状数组)

    首先,要先讲讲树状数组: 树状数组(Binary Indexed Tree(BIT), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据结构.主要用于查询任意两位之间的所有元素之 ...

  10. 树状数组-HDU1541-Stars一维树状数组 POJ1195-Mobile phones-二维树状数组

    树状数组,学长很早之前讲过,最近才重视起来,enmmmm... 树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)是一个查询和修改复杂度都为log(n)的数据 ...

随机推荐

  1. Maven详解(八)------ 继承和聚合

    1.继承 需求场景: 有三个 Maven 工程,每个工程都依赖某个 jar 包,比如 Junit,由于 test 范围的依赖不能传递,它必然会分散在每个工程中,而且每个工程的jar 包版本可能不一致. ...

  2. 2D 和 3D 中的 CSS 轉換 (Preliminary) CSS3中 translate3D详解

    http://www.zhangxinxu.com/wordpress/2012/09/css3-3d-transform-perspective-animate-transition/ http:/ ...

  3. 迈向angularjs2系列(6):路由机制

    目录1.angular-seed的路由2.路由机制的探索3.懒加载 一:angular-seed的路由 step1:安装种子项目    $ git clone --depth 1 https://gi ...

  4. Ubuntu14.04下搭建VPN服务 -pptp

    在Ubantu下采用PPTP搭建VPN,优点是配置简单快捷.本教程亲自测试,熟练了在新机器上5分钟搞定VPN. - - - - - - - - - - - - - - - - - - - - - - ...

  5. MongoDB基本shell操作

    ---------------------MongoDB基本操作--------------------- 1.MongoDB创建数据库     use 数据库名:切换到指定的数据库中,在插入第一个条 ...

  6. Selenium+IDEA+Maven+TestNG环境搭建

    第一 安装java环境. 1. 下载并安装Jdk1.7或Jdk1.8 http://www.oracle.com/technetwork/java/javase/downloads/index.htm ...

  7. docker+tomcat 启动时非常慢原因之JRE /dev/random阻塞

    docker+tomcat 启动时非常慢,一般正常启动几十秒的,发现docker+tomcat启动竟需要几分钟,不可思议 根本原因是 SecureRandom 这个 jre 的工具类的问题.那为什么 ...

  8. [转]Java中的随机数生成器:Random,ThreadLocalRandom,SecureRandom

     详见: http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp84 Random即:java.util.Random, ThreadL ...

  9. Hash表分析

    http://baike.baidu.com/link?url=Ua74895uGf1NuPxB4pawmuAXedi427jJvM6aSLh_V1-23ptlMc7XIrr_cylIBn5d

  10. 数据绑定技术一:GridView控件

    在网站或应用程序中,要显示数据信息,可用到ASP.NET提供的数据源控件和能够显示数据的控件. 一.数据源控件 数据源控件用于连接数据源.从数据源中读取数据以及把数据写入数据源. 1.数据源控件特点 ...