二叉索引树

作者: 赵阳_c149 | 来源:发表于2019-07-24 14:42 被阅读1次

    概述和用途

    二叉索引树(Binary Index Tree 简称BIT)又称树状数组或者Fenwick树,最早由Peter M. Fenwick与1994年发表。其初衷是解决数据压缩离的累积率(Cumulative Frequency)的计算问题,现在多用于高效计算数列的前缀和,区间和。构建BIT的时间复杂度为O(NlogN),空间复杂度为O(N)。搜索和更新的复杂度为O(logN)

    含义

    假设有一个数组a[i],长度为N,数组的每个元素存储着一个数字。


    image.png

    由于某种原因,我们需要得到一个新的数组sum[i],sum[i]数组中的每个元素都是数组a[i]中相应元素的前缀和,换句话说,sum[i]的值等于从a[0]到a[i]的累加和,表示成公式就是:
    sum[i] = ∑a[j], 0<=j<=i。


    image.png
    sum[i]检索起来非常方便,时间复杂度为O(1),但是问题是如果a[i]会频繁的进行更新,那么相应的sum[i]数组也必须随之更新,每次更新的时间复杂度为O(N),特别是在N相当大的情况下,程序的效率将急剧下降。

    因此必须找到一个更好的方法来解决效率的问题。建立一个树,

    1. 树的根节点index为0,是一个dummy节点,并不存储任何信息。这个树有7个节点,index为1~7,分别对应原数组a[i]的元素0~6也就是说树的总节点数比原数组a[i]的元素个数大一。

    2. 注意到每一个节点和他的子节点有着一致的对应关系。

    例如:
    节点1,用二进制表示:01,如果将最后一位1移除,则得到00
    节点2,用二进制表示:10,如果将最后一位1移除,则得到00
    节点4,用二进制表示:100,如果将最后一位1移除,则得到00
    而他们的父节点0,用二进制表示:00
    
    再看:
    节点5,用二进制表示:101,如果将最后一位1移除,则得到100
    节点6,用二进制表示:110,如果将最后一位1移除,则得到100
    而他们的父节点4,用二进制表示:100
    
    1. 对于每个节点,其index都可以表示成2i1+2i2+…+2in的形式。例如:
    1 = 0 + 2^0
    2 = 0 + 2^1
    4 = 0 + 2^2
    5 = 2^2 + 2^0
    6 = 2^2 + 2^1
    7 = 2^2 + 2^1 + 2^0
    

    根据这一规律,计算每个节点存储的值。

    节点1: a[0] 
    节点2: a[0] + a[1]
    节点4: a[0] + a[1] + a[2] + a[3]
    节点5: a[4]
    节点6: a[4] + a[5]
    节点7: a[6] 
    

    通过以上步骤,我们建立起了一个BIT:


    image.png

    利用BIT计算前缀和

    现在,考虑如取得想要的前缀和,对于树中的任意一个节点,将其表示为T[i],1<=i<=7,

    sum[4] = T[5] + T[4] = 16
    sum[6] = T[7] + T[6] + T[4] = 2 + 4 +11 = 17
    

    也就是说,只需从树上对应的节点,回溯至跟节点,并将路径上所有节点的数值累加,即可得到前缀和。时间复杂度为O(logN)。如果一时想不通为什么,可以试着考虑节点右上角(start,end)的含义,即每个节点代表了数组上某一跨度范围内的元素的累加和。

    用算法构建和更新BIT

    接下来,考虑用算法构建和更新这个树。

    对于构建来说,插入节点的过程就是不断更新树的过程,所以首先考虑更新树。每更新一个节点,则只需更新其后的兄弟节点,以及其所有祖先节点之后的所有兄弟节点。
    假设我们需要更新a[2]的值,需更新T[3]和T[4]的值;如果需要更新a[4]的值,则需更新T[5]和T[6]的值。也就是说,如果更新a[i] 的值,则应该更新T[j1] ,T[j2],… T[jk]的值。其中,j1 = i + m1,m1是这样得到的:

    1. 将i表示为二进制,取从低位开始第一个不为0的位,假设为b1
    2. m1= 2 ^(b1-1)
    
    j2 = j1 + m2,
    1.将j1表示为二进制,取从低位开始第一个不为0的位,假设为b2,
    2.m2= 2 ^(b2-1)
    
    以此类推,直到jk+1 > N。
    

    我知道看到这里,你可能会有两种反应:
    1.大脑有些缺氧。
    2.王之蔑视。
    的确,熟悉C语言的同学应该已经想起简洁明了的位运算了:

    nextPos = currentPos + (currentPos & -currentPos)
    

    写成代码:

        private static int[] buildBIT(int[] origin){
    
            int[] bitArray = new int[origin.length + 1];
    
            for(int i = 0; i<bitArray.length; i++){
                bitArray[i] = 0;
            }
    
            for(int i = 1; i< bitArray.length; i++){
                updateBitArray(origin[i-1], i, bitArray);
            }
    
            return bitArray;
        }
    
        private static void updateBitArray(int valueDelta, int bitArrayPos, int[] bitArray){
    
            bitArray[bitArrayPos] += valueDelta;
    
            // get next position in bit array, change it
            int next = getNext(bitArrayPos);
            while(next < bitArray.length){
                bitArray[next] += valueDelta;
                next = getNext(next);
            }
        }
    
        private static int getNext(int cur){
            return cur + (cur & -cur);
        }
    

    时间复杂度ON(logN),空间复杂度O(N)。

    用算法计算前缀和

    最后,来看如何对树进行检索,也就是说,给定一个i,求sum[i] = ∑a[j], 0<=j<=i。

    根据前面的说明,不难得知,只需要找到节点T[i+1],回溯至跟节点,并将路径上所有节点的数值累加,即可得到前缀和。这里同样要借助位运算:

    parentPos = currentPos -(currentPos & -currentPos)
    

    写成代码:

        private static int searchSum(int[] bitArray, int cur){
            int sum = bitArray[cur];
            int parent = getParent(cur);
            while(parent > 0){
                sum += bitArray[parent];
                parent = getParent(parent);
            }
            return sum;
        }
    

    时间复杂度O(logN),空间复杂度O(1)。

    树状数组
    Tushar Roy 小哥的专栏

    相关文章

      网友评论

        本文标题:二叉索引树

        本文链接:https://www.haomeiwen.com/subject/zdsxrctx.html