Data Structure: Binary Index Tree

来源:互联网 发布:天海翼 知乎 编辑:程序博客网 时间:2024/05/23 01:58

Binary Index Tree适合用以Range Query. 例如:求一个大区间的某个小区间的和。


为什么要使用Binary Index Tree?


先看看,如果不使用Binary Index Tree的话,我们可以使用的方法。


1)直接求。那么复杂度是O(mn)。m是查询次数,n是大区间的元素个数。

2)使用sum的array,建立一个array,每个index的值代表原来的array到当前的index的和。建立这么一个array需要时间负责度O(n*n),查询复杂度O(1),但是Update复杂度是O(n*n)。 空间复杂度O(n)。

3)Segment Tree,也是比较科学的办法。


现在,我们来介绍一下另一种神奇的数据结构。Binary Index Tree,每个节点代表的含义是:当前节点的编号是父节点编号flip the right most bit的值。每个节点的值是当前节点编号的2进制编码分解后,根据分解结果求的某个子区间的sum值。


Binary Index Tree实际大小只是原来数组的长度 + 1,不同于Segment Tree的4*n的大小。实质上只是在原来的数组的基础上一个dummy node,然后重新根据求解问题的性质(譬如求和),组织、合并原来数组的信息。


首先,我们来看节点编号和节点相对层次(位置)的关系,编号关系是如何决定谁是谁的父节点,谁是谁的子节点。


图片、例子来自:https://www.youtube.com/watch?v=CWDQJGaN1gY




例如,我们要把原数组转换为下面的Binary Index Tree。为什么要这么安排呢?因为0的二进制是00000000,1是00000001, 二是00000010,4是00000100,8是00001000。很明显,只要把上面的数的right most bit 翻转以下,就是0.


同理,3是00000011,翻转最右bit,则是00000010,是2. 以此类推。


很明显,这不是一颗完全二叉树,所以parent和children的索引值关系不能用2i + 1或者+2来表示。那么,在二叉索引树的这里,怎么求一个节点的父节点呢?(求父节点在求解区间和的问题上是必须用到的)。


分三步:


1) 求当前节点编号的Two Complement。(按位求反之后+1)

2)于当前的节点编号按位与。

3)用当前节点编号减去把上述两步得到的值。


得到的新编号就是父节点的编号。以3为例。


1)00000011 -> 11111101

2)00000011 & 11111101 = 00000001

3)00000011 - 1 = 2. 


实质上,我们只是新建了一个长度 + 1的数组,只不过,我们用树的形式把这个新的数组解释了以下。


那么,各个节点的值如何决定?根据二进制的表示形式。区间的左边值取自加号左边的实际值,右边值取自于2的次方数 + 左边值。


1 = 0 + 2^0, 则表示[0,0]的值,3。 

2表示为0 + 2^1,表示[0, 1]的和值,5。

那么3呢? 2^1 + 2^0 = 2 + 2^0,则表示[2, 2+0]. 

同理,7 = 2^2 + 2^1 + 2^0,则加号左边是:6. 所以区间为[6, 6]. 


所以,填完所有的值后,Binary Index Tree以这样的形式出现:




但很明显,这么填充值的方法十分麻烦。那有没有更方便的方法呢?


有。记住,我们创建的新旧两个数组之间,旧的索引值+1得到的新的索引值之间的两个元素是有密切关系的(新的数组只是增加了dummy node)。


那么,我们去读每个旧数组的元素,每一次首先把新数组对应的索引值上的元素值加上这个元素值。然后,用getNext()方法求哪些其它新数组的元素也要加上这个值。


getNext的逻辑与parent一点类似,要注意区分:


1)求原来(新索引)编号的 two's complement;

2)按位与原来的编号

3)加上原来的编号


注意,只是第三步不一样。但是两步并不是互逆的。


每次更新值的时候,先更新第一个新索引(旧的+1的值)。然后不断getNext得到需要更新的其它索引值。直到getNext返回的值在新数组范围之外。


最后,我们来说一下如何解决上面的区间和的问题。譬如,我们现在要求[0, 5]的区间和。那么,5对应到新索引值6,然后从6的值开始,不断加上getParent()得到的索引值对应的元素值。直到getParent()得到的是0. 


代码如下:


/** * According to this implementation, it can only be convenient when * we are computing the sum from index 0.  * @author jasli * */public class Solution {private int[] indexTree;private int getParent(int cur) {int mask = ~cur + 1;int minus = cur & mask;return cur - minus;}private int getNext(int cur) {int mask = ~cur + 1;int add = cur & mask;return cur + add;}public void update(int[] data, int index) {if (indexTree == null) {return;}int currentIndex = index + 1;int n = indexTree.length;while (currentIndex < n) {indexTree[currentIndex] += data[index];currentIndex = getNext(currentIndex);}}public int getSum(int index) {if (indexTree == null) {return 0;}int currentIndex = index + 1;int sum = 0;while (currentIndex > 0) {sum += indexTree[currentIndex];currentIndex = getParent(currentIndex);}return sum;}public void createTree(int[] data) {int n = data.length;indexTree = new int[n + 1];for (int i = 0; i < n; ++i) {update(data, i);}}public static void main(String[] args) {// TODO Auto-generated method stubint[] tmp = new int[]{3, 2, -1, 6, 5, 4, -3, 3, 7, 2};Solution ins = new Solution();ins.createTree(tmp);System.out.println(ins.getSum(5));}}