树状数组(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. JVM堆内存设置

    今天碰到了一个题目,讲的是关于堆内存的问题,题目如下   下面哪种情况会导致持久区jvm堆内存溢出? A.循环上万次的字符串处理 B.在一段代码内申请上百M甚至上G的内存 C.使用CGLib技术直接操 ...

  2. VMware Workstation 12 Pro 之安装林耐斯Ubuntu X64系统

    VMware Workstation 12 Pro 之安装林耐斯Ubuntu X64系统... -------------- Linux依照国际音标应该是/'linэks/——类似于“里讷克斯&quo ...

  3. 9.19.3 反射和Properties(重要)

    dbinfo.properties文件中的内容:     driver oracle.jdbc.driver.OracleDriver url=jdbc:oracle:thin:@192.168.1. ...

  4. Java基础二

    1 关键字 定义:被java语言赋予了特殊含义的单词. 特点:关键字中的所有字母都为小写. 用于定义数据类型的关键字 class.interface.byte.short.int.long.float ...

  5. Python3调用企业微信用于告警

    前段时间利用py爬虫抓取一些网页信息,然后通过wxpy发送到微信群,以用作日常告警,感觉还是很方便. 但好景不长,我的小号微信被腾讯封了(很常见咯), 显示无法登录网页版微信,至今已经有半个多月了. ...

  6. 关于模式为singTask的activity的bundle问题

    1.问题描述 今天遇到一个问题,我的一个页面跳模式为singTask的activity时,bundle的值拿不到,我于是找了下,发现基础真的很重要; 2.原因 launchMode为singleTas ...

  7. 源码编译安装bind

    author:JevonWei 版权声明:原创作品 编译bind 准备阶段: 下载bind软件包,然后传输到系统中 https://www.isc.org/downloads/ 安装开发包组 yum ...

  8. poj 1679 Prim判断次短路

    题意:判断最短路是否唯一. 思路:先prrim一次求出最短路同时记录最短路加入的边: 然后枚举所求边,将其删除再求n-1次prim,判断再次所求得的最短路与第一次求得的次短路的关系. 代码: #inc ...

  9. 【★】KMP算法完整教程

    KMP算法完整教程 全称:                               Knuth_Morris_Pratt Algorithm(KMP算法) 类型:                 ...

  10. 在对话框中利用CToolBar类添加工具条的方法

    UINT BASED_CODE DockTool[]={ID_NEWGAME,ID_SAVE,ID_OPEN,ID_SEPARATOR,ID_COPYFEN,ID_PASTEFEN,ID_SEPARA ...