树状数组

树状数组(Binary Indexed Trees or Fenwick_tree)

介绍

Wiki中对树状数组的介绍说:按照Peter M. Fenwick的说法,正如所有的整数都可以表示成2的幂和,我们也可以把一串序列表示成一系列子序列的和。采用这个想法,我们可将一个前缀和划分成多个子序列的和,而划分的方法与数的2的幂和具有极其相似的方式。一方面,子序列的个数是其二进制表示中1的个数,另一方面,子序列代表的f[i]的个数也是2的幂。

可以看出树状数组展现了位运算与数组结合的神奇魅力。那么,树状数组到底有什么用呢?

引入

下面举个树状数组的经典应用:区间求和。

假设我们有如下数组(数组元素从 index=1 开始):

 var a = [X, 1, 2, 3, 4, 5, 6, 7, 8, 9];

我们设定两种操作,

  1. modify(index, x) 表示将 a[index] 元素加上x,
  2. query(n, m) 表示求解 a[n] ~ a[m] 之间元素的和。

如果不了解树状数组(当然假设更不了解线段树等其他数据结构),你可能会很容易地写下如下代码:

var a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

function query(n, m) {
  var sum = 0;
  for (var i = n; i <= m; i++){
     sum += a[i];
}
  return sum;
}

function modify(index, x) {
  a[index] += x;
}

Ok,复杂度为O(1)的删改和复杂度为O(n)的查询。

如果数据量很大,这样反复的查询是相当耗时的。我们退一步想,如果只有 query(n, m) 这个操作,很容易想到用sum数组预处理前n项的和,然后用 sum[m] - sum[n-1] 获得答案。但是如果要修改 a[index] 的值,因为该项影响所有index之后的sum数组元素,所以如果这样做复杂度变为O(1)的查询和O(n)的删改,并没有什么实际用处。

出场

但是,我们可以用一个sum数组保存一段特定的区间段的值。假设我们有 a[1] ~ a[9] 9个元素,我们根据一个特定的规则:

sum[1] = a[1];
sum[2] = a[1] + a[2];
sum[3] = a[3];
sum[4] = a[1] + a[2] + a[3] + a[4];
sum[5] = a[5];
sum[6] = a[5] + a[6];
sum[7] = a[7];
sum[8] = a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7] + a[8];
sum[9] = a[9];

如果求 a[1] ~ a[9] 的和,即为 sum[9] + sum[8],
如果求 a[1] ~ a[7] 的和,即为 sum[7] + sum[6] + sum[4] ,
如果要改变 a[1] 的值,改变sum数组中和 a[1] 有关的项即可(即 sum[1],sum[2],sum[4],sum[8])。

这就是树状数组!实现了O(logn)的查询和删改。但是如何将a数组和sum数组联系起来?

说明

来观察这个图:
这里写图片描述

令这棵树的结点编号为C1,C2…Cn。令每个结点的值为这棵树的值的总和,那么容易发现(如上所说):

C1 = A1
C2 = A1 + A2
C3 = A3
C4 = A1 + A2 + A3 + A4
C5 = A5
C6 = A5 + A6
C7 = A7
C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8

解释:
A[]: 保存原始数据的数组
C[]: 树状数组,其中的任意一个元素C[i]可能是一个或者多个A数组中元素的和。
C[i]中的元素:如果数字 i 的二进制表示中末尾有k个连续的0,则C[i]是A数组中2^k个元素的和,则C[i]=A[i-2^k+1]+A[i-2^k+2]+…+A[i-1]+A[i]。也就是说,C[i]中每一个元素管理着A[]中若干个元素的和,并且各个元素管理的区间没有重叠。
所以公式:Cn = A(n – 2^k + 1) + ... + An

如: 4=100(2)  C[4]=A[1]+A[2]+A[3]+A[4];
  6=110(2)  C[6]=A[5]+A[6]
  7=111(2)  C[7]=A[7]

      
计算2^k的两个方法:

2^k = (i & (-i)); (利用机器补码特性)
2^k = (i & (i^(i-1));

代码:

int lowbit(int x) {
      return x & (-x);
}

当想要查询一个SUM(n)(求a[1]~a[n]的和),可以依据如下算法即可:

  1. 令sum = 0,转第二步;
  2. 假如n <= 0,算法结束,返回sum值,否则sum = sum + Cn,转第三步;
  3. 令n = n – lowbit(n),转第二步。

可以看出,这个算法就是将这一个个区间的和全部加起来。

那么修改呢,修改一个节点,必须修改其所有祖先,最坏情况下为修改第一个元素,最多有log(n)的祖先。所以修改算法如下(给某个结点i加上x):

  1. 当i n时,算法结束,否则转第二步;
  2. Ci = Ci + x, i = i + lowbit(i)转第一步。i = i + lowbit(i)这个过程实际上也只是一个把末尾1补为0的过程。

对于数组求和来说树状数组简直太快了!

举个栗子:

i=6=0110;
可以发现末尾连续的0有一个,即k=1,则说明C[6]是在树中的第二层,并且C[6]中有2项,随后我们求出了起始项:
A[6-2+1]=A[5],
但是在编码中求出k的值还是有点麻烦的,所以我们采用更灵巧的Lowbit技术,即:2k=i&-i 。
则:C[6]=A[6-2+1]=A[6-(6&-6)+1]=A[5]+A[6]。

编码

  • Lowbit函数
    // 当前的sum数列的起始下标
    public static int Lowbit(int i) {
        return i & -i;
    }
  • 求前n项和

比如上图中,如何求Sum(6),很显然Sum(6)=C4+C6,那么如何寻找C4呢?即找到6以前的所有最大子树,很显然这个求和的复杂度为logN。


    // 求前n项和
    public static int Sum(int x) {
        int ans = 0;
        int i = x;
        while (i > 0) {
            ans += sumArray[i - 1];
            //当前项的最大子树
            i -= Lowbit(i);
        }

        return ans;
    }
  • 修改

如上图中,如果我修改了A[5]的值,那么包含A[5]的C[5],C[6],C[8]的区间值都需要同步修改,我们看到只要沿着C[5]一直回溯到根即可,同样它的时间复杂度也为logN。

    public static void Modify(int x, int newValue) {
        //拿出原数组的值
        int oldValue = arr[x];
        arr[x] = newValue;
        for (int i = x; i < arr.length; i += Lowbit(i + 1)) {
            //减去老值,换一个新值
            sumArray[i] = sumArray[i] - oldValue + newValue;
        }
    }

整体项目代码见我的Github

结果

这里写图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值