树状数组(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. Swift语言中与C/C++和Java不同的语法(五)

    这一节将会提到Swift中其他的关于函数的应用,这些内容包括有: 默认参数值和可变参数值.常量参数.返回函数类型和函数嵌套: 一.默认参数值和可变参数值 对默认参数值是指在函数的声明中就已经定义了参数 ...

  2. linux下权限问题思考

    今天遇到一些关于linux的权限问题,文件的所有者,文件的所属组等问题,文件对于所有者所属组是非常敏感的,同一个脚本所属者所属组不同,得到执行的结果也是差很多的.

  3. poj2828 Buy ticket

    Description Railway tickets were difficult to buy around the Lunar New Year in China, so we must get ...

  4. Day4 - Linux分区规划与xshell使用排错

    1.1 没有重要数据 /boot   200M    存放系统的引导信息 内核 swap   交换分区   防止内存用光了 临时的一个内存 如果你的内存小于8G swap是内存的1.5倍   如果你的 ...

  5. Haproxy基于ACL做访问控制

    author:JevonWei 版权声明:原创作品 haproxy配置文档 https://cbonte.github.io/haproxy-dconv/ 基于ACL做访问控制(四层代理) 网络拓扑 ...

  6. 极化码的matlab仿真(3)——SC译码(1)

    一个好码必须具备两个要素:可靠.高效. 高效的码要求码的编译方案都具有较低的复杂度.极化码出现后,Arikan本人提出使用SC译码方案来进行译码操作.SC全称successive cancellati ...

  7. Vue双向数据绑定原理解析

    基本原理 Vue.采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter和getter,数据变动时发布消息给订阅者,触发相应函数的回调 ...

  8. 最近见到的JS返回函数的一些题

    JS返回值题一直都是考察重点,面试和笔试之中也经常涉及到,说一说我最近遇到的一些有意思的JS返回函数问题. 之前见到过一道有意思的问题,说有一个sum函数,用户可以通过sum(2,3)来取到2+3 = ...

  9. 聊聊click延迟和点击穿透

    博客原文地址:Claiyre的个人博客 https://claiyre.github.io/ 如需转载,请在文章开头注明原文地址 移动端click事件被延迟 移动端的开发经常需要监听用户的双击行为,所 ...

  10. 团队作业8——第二次项目冲刺(Beta阶段)--第二天

    团队作业8--第二次项目冲刺(Beta阶段)--第二天 会议照片: 燃尽图: 项目进展: 今天完成了记录用户的姓名,其他的任务还在跟进. 团队贡献比: 队员 角色 团队贡献比 陈麟凤 PM 16% 张 ...