Data Structure: Binary Index Tree(LC308作为例子)

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值。


所以,对应的坐标关系是:


int child = parent + (parent & (-parent));


注意,正整数的补码是其负数的表示(反码+1 = 补码)。所以,parent + 后面括号的结果就是flip the right most bit. 


反之,parent = child - (child & (-child));


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


所以,要填充或者查找binary index tree的话,都是以当前index + 1作为起点去递归的。


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


图片、例子来自: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返回的值在新数组范围之外。


得知求子节点和父节点的方法后,剩下的便是在创建binary index tree之后更新的问题。注意,更新和创建tree一样,都是通过改变后的数值和当前数值的改变量(所以buf是用来复制当前数组的存在来求改变量的),从当前的index + 1的位置,层层递归下去(找子节点)来完成的。参考LC307代码即可找到规律:


需要注意的是,求sum的话,index + 1代表包含当前index的sum,属于suffix sum。所以,如果想通过sum相减得到某个区间[i, j]和,应该sumJ 从j + 1开始,sumI从i开始。


class NumArray {
    private int[] tree;
    private int[] buf;
    private int n;
    
    public NumArray(int[] nums) {
        if (nums == null || nums.length == 0) {
            return;    
        }
        
        n = nums.length;
        tree = new int[n + 1];
        buf = new int[n];
        
        for (int i = 0; i < n; ++i) {
            update(i, nums[i]);
        }
    }
    
    public void update(int i, int val) {
        if (n == 0) {
            return;
        }
        int cur = i + 1;
        int delta = val - buf[i];
        buf[i] = val;
        for (int j = cur; j <= n; j = j + (j & (-j))) {
            tree[j] += delta;
        }
    }
    
    public int sumRange(int i, int j) {
        if (n == 0) {
            return 0;
        }
        
        int sumI = 0;
        int sumJ = 0;       
        
        
        for (int k = i; k > 0; k = k - (k & (-k))) {
            sumI += tree[k];
        }
        for (int k = j + 1; k > 0; k = k - (k & (-k))) {
            sumJ += tree[k];
        }
        
        return sumJ - sumI;
    }
}


下面的结合LC308的代码来说明:


308. Range Sum Query 2D - Mutable


Given a 2D matrix matrix, find the sum of the elements inside the rectangle defined by its upper left corner (row1, col1) and lower right corner (row2, col2).

Range Sum Query 2D
The above rectangle (with the red border) is defined by (row1, col1) = (2, 1) and (row2, col2) = (4, 3), which contains sum = 8.

Example:
Given matrix = [
  [3, 0, 1, 4, 2],
  [5, 6, 3, 2, 1],
  [1, 2, 0, 1, 5],
  [4, 1, 0, 1, 7],
  [1, 0, 3, 0, 5]
]


sumRegion(2, 1, 4, 3) -> 8
update(3, 2, 2)
sumRegion(2, 1, 4, 3) -> 10


注意的点:sumRect的时候index的问题。注意index tree和prefix sum的意义是相同的。int[x][y]不包含matrix[x][y]这个元素。如果是一维的话可以直接通过i = row + 1这种方式。但是,本次计算二维的时候,有些时候,行和列include, exclude是不同的,所以这里比较特别。


代码:

class NumMatrix {
    int[][] tree;
    int[][] buff;
    int m;
    int n;

    public NumMatrix(int[][] matrix) {
        if (matrix == null || matrix.length == 0 || matrix[0] == null || matrix[0].length == 0) {
	return;
        }
        m = matrix.length;
        n = matrix[0].length;
        tree = new int[m + 1][n + 1];
        buff = new int[m][n];

        for (int i = 0; i < m; ++i) {
	    for (int j = 0; j < n; ++j) {
		update(i, j, matrix[i][j]);
	    }
         }
    }
    
    public void update(int row, int col, int val) {
        if (m == 0 || n == 0) {
	return;
        }
        int delta = val - buff[row][col];
        buff[row][col] = val;
        for (int i = row + 1; i <= m; i = i + (i & (-i))) {
	for (int j = col + 1; j <= n; j = j + (j & (-j))) {
                tree[i][j] += delta;
            }
       }
    }
    
    private int sumRect(int row, int col) {
        int res = 0;
        for (int i = row; i > 0; i = i - (i & (-i))) {
            for (int j = col; j > 0; j = j - (j & (-j))) {
                res += tree[i][j];
            }
       }
       return res;
    }

    public int sumRegion(int row1, int col1, int row2, int col2) {
        if (m ==0 || n == 0) {
	return 0;
        }
        return (sumRect(row2 + 1, col2 + 1) - sumRect(row2 + 1, col1) - sumRect(row1, col2 + 1) + sumRect(row1, col1)); 
    }
}



  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值