我们学习数据结构的目的在于将我们的算法变得更快。由 Peter M. Fenwick 提出的树状数组 BIT 结构就是一个优秀的数据结构,BIT 全称 Binary Indexed Trees 结构,而不是所说的比特奥。Peter M. Fenwick 首次使用此结构进行数据压缩。在算法竞赛中,通常用于存储频率和处理累积频率表。

首先考虑一个简单的问题。

给定一个数组 arr[0 ... n-1] ,如何实现下面两个操作:

  1. 计算前 i 个元素的累加和;

  2. 将数组中下标为 i 的元素的值更新为 x,arr[i] = x ,其中 0 <= i <= n-1

一个简单的方法就是遍历 0 到 i - 1 的元素并计算出累加和即可 ;然后更新操作 arr[i] = x 就可以直接进行,也就说可以对数组 arr[] 直接进行修改.

// 计算前 i 个元素的累加和
public int getSum(int arr[], int i){int sum = 0;for(int j = 0; j < i; j++){sum += arr[j];}return sum;
}

这种方式第一个操作,也就是计算累加和的时间复杂度为 ,更新操作的时间复杂度为 ;

另外一种方式就是创建一个大小为 n 的新数组,并且在新数组的第 i 个位置保存前 i 个元素的累加和。此时查找给定范围内的累加和就可以在 的时间内完成,但是更新操作将花费 的时间,这对于大量的查询操作,而更新操作比较少的问题很实用。

// 更新数组 arr[i] = x 之后
// 需要对存储累加和的数组 new_arr 进行的修改。
void updateSum(int arr[], int i, int x){arr[i] = x;for(int j = i; j < new_arr.length; j++){new_arr[j] = new_arr[j-1] + arr[j];}
}

也就说,要实现上面提到的两个操作,要么查找为 ,更新操作为 ;要么使用额外的空间,将查找操作降为 ,但是更新操作变为了 .

树状数组

那么是否可以将查找和更新操作同时降低到 呢?

一个就是以后会讲到的线段树(Segment Tree),另外一个就是树状数组 (Binary Indexed Tree),两者均可以将上面所提到的查找和更新操作的时间复杂度降到 。但是与线段树相比,树状数组的效率更高,并且易于实现。

树状数组表示为 BITree[];树状数组的每个节点存储输入数组中某些元素的和;树状数组的大小等于输入数组的大小,记作 n 。为了便于实现,BITree[] 使用 n+1 的大小。

首先,我们给出一个数组 arr[] :

然后直接直观地看一下针对这个数组 arr[] 的树状数组:

事实上这棵树并不存在,树状数组依然只是下面的一个数组而已:

现在的问题是如何从原始数组 arr[] 得出树状数组 BITree[] 呢?

答案很简单:

  1. 首先将树状数组 BITree[] 的所有元素初始化为 0;

  2. 调用 updateBITree() 函数更新 BITree[] 数组即可。

所以关键就是实现  updateBITree() 函数啦!

实现(敲代码)不是关键,重要的是理解为什么!

我们先来细致地看一趟   updateBITree() 函数的执行过程:

第一步:index = 1 ,将 BITree[1] = BITree[1] + arr[0] :

第二步:更新 index = index + index & (-index) = 1 + 1 = 2 ,这里你可能一头雾水,没关系,这篇文章最后没有让你彻底明白树状数组,你大可喷我!我暂且不解释它的含义和作用,我们仅仅解释一下 index & (-index) 表示什么。index & (-index) 表示将 index 所代表的值转化为二进制之后,从右向左数,第一个 1 的位置,例如 6 & (-6) ,6 的二进制为 110 ,从右向左数,第一个 1 的位置是 2 ,那么 6 & (-6) = 2 。当然这是二进制运算之中取最后一个 1 的小诀窍,下面是的,以一个32位的机器为例:

这里如果有问题,大家可以看一下 剑指 offer 面试题精选图解 15 . 二进制中1的个数 这篇文章,然后复习一下原码、反码和补码接着看。

第三步:index = 2 ,将 BITree[2] = BITree[2] + arr[0] :

第四步:更新 index = index + index & (-index) = 2 + 2 = 4

第五步:index = 4 ,将 BITree[4] = BITree[4] + arr[0] :

第六步:更新 index = index + index & (-index) = 4 + 4 = 8

第七步:index = 8 ,将 BITree[8] = BITree[8] + arr[0] :

第八步:更新 index = index + index & (-index) = 8 + 8 = 1616 > 12 ,已经超出了树状数组 BITree[] 的下标,一趟  updateBITree() 函数的执行结束啦!知道你没啥感觉更是没有体会到树状数组的妙用(我刚开始也是,说实话,笨笨的大禹看了好几天)。

但是当你将所有的步骤都都走完之后,你就会感觉不一样啦!

图中没有填充的单元格都表示 0,第 1 趟  updateBITree() 函数确定了 BITree[1] 的值,第 2 趟 updateBITree() 函数确定了 BITree[2] 的值,以此类推,第 12 趟  updateBITree() 函数确定了 BITree[12] 的值,也就是结果 12 (12就是数组 arr[] 的大小)趟更新,我们得到了我们的主角 BITree[] 树状数组:

也就是,我们完成了从数组  arr[]  到 BITree[] 的过渡。

下面我要告诉你的才是树状数组的关键和核心奥!

树状数组的关键不是 BITree[] ,而是 下标

假设现在的原始数组 arr[] 的大小 n = 16 ,我们看下标 1 到 16 到底如何成为树状数组的关键所在的。

对于上面的每一个 index , 均计算 index & (-index) 的值,比如 10,可以计算得到 10 & (-10) = 2 ,实在不会也没关系,就把 10 转化为二进制 1010 ,然后从右向左数数,碰到的第一个 1 的位置就是 2 (其他数字的计算都是一样的过程,就不过多说明)。

而这个  index & (-index) 所对应的值有何意义呢?

答案,index & (-index) 表示一个范围,千篇一律的叫法叫做 Lowbit(index)

index & (-index) 中的第一个 1 为例,它表示将数组 arr[] 中当前位置向前累加 1 个数字,作为  BITree[index] 的值,即 BITree[1] = 2 .

那么 BITree[] 数组中的值 30 的由来就更好解释了,就是从当前元素 9 向前累加 4 个元素(包含自身),即 9 + 8 + 7 + 6 = 30

对于 index & (-index) 中的其他元素的解释是同样的道理。但是 index & (-index) 所表示的数组你以为就这样简单吗?若真是如此,估计我就不讲了。

就一棵树而言,必定有父子之分,那么树状数组是如何体现父子关系的呢?

  • BITree[y]BITree[x] 的父结点,当且仅当 y 可以通过从 x 的二进制表示中删除最后一个位置的 1 (也就是从右向左第一个) 来获得,即 y = x - (x & (-x))

有了这样的父子关系,仅使用  index & (-index) 就可以直观地构建出我们期待已久的树状数组中所谓的树。

已知 index 和   index & (-index) ,计算两者之差简直轻而易举:

那么构建一颗树还难吗?一点儿都不。

比如 y 等于 0 ,视线向上找到对应的 index,分别为 1、2,4、8、16,也就是说,0是 1、2、4、8、16 的父结点;

同理,2 是 3 的父结点、4 是 5 和 6 的父结点、6 是 7 的父结点、8 是 9 和 10 的父结点,10 是 11 和12 的父结点、12 是 13 和 14 的父结点,14 是 15 的父结点。

就得到了下图:

这棵树的得出与原数组 arr[] 本身没有关系,而仅仅与下标 index 有关。而我们最开始所看到的树同样如此(只不过树中结点的真正的值是我们所计算出的 BITree[index]

树状数组的几大特点:

  1. BITree[0] 是一个虚拟结点,同时也是我们所看到的根结点

  2. BITree[y]BITree[x] 的父结点,当且仅当 y 可以通过从 x 的二进制表示中删除最后一个位置的 1 (也就是从右向左第一个) 来获得,即 y = x - (x & (-x))

  3. BITree[y] 的孩子结点 BITree[x] 存储的是数组 arr[] 中下标从 y (包含) 到 x (不包含) 的累加和,即 arr[y,...,x) ,注意括号是不包含 x;

关于这个第三条可能需要稍微解释一下:

BITree[8] 的孩子结点 BITree[12] 的值等于 30 ,表示数组 arr[] 中下标从 812(不包含 12)的元素的累加和,即  BITree[12] = 30 = arr[8,...,12) = 6 + 7 + 8 + 9

其实这里就和之前我们介绍的  index & (-index) 所表示含义不谋而合。

是不是有点儿清晰呢?很快你就会看到一句话概括上面所讲的所有内容。

回到我们最开始的两个问题。

如何根据 BITree[]  树状数组,获取数组 arr[] 中前 i 个元素的累加和?

这里更关键奥!!!

我们都知道,任何一个正整数都可以被表示为 2 的次幂和,比如 11 可以表示为 8 + 2 + 1. BITree的每个节点都存储 n 个元素的总和,其中 n 是 2 的次幂。比如前 11 个元素的累加和可以通过对原数组 arr[] 中最后 1 个元素(第11个元素)、向前两个元素(第 9 和 10 号元素)和 前 8 个元素 (从 1 到 8 的)的元素之和求得。

对照上图,来理解文字描述就更清晰了,我们求前 11 个元素的累加和,可以将其分解为 2 的次幂之和,即 8 + 2 + 1,也就是前 8 个元素的累加和(1 到 8),紧挨着的 2 个元素(9 和 10),和最后 1个元素 (11)三者的和。

如果从树状数组的角度来看,BITree[8] = 21  表示前 8 个元素的累加和,BITree[10] = 13  表示 6 和 7 的和(这里解释一下, 表示的就是两个数的和), BITree[11] = 8  表示一个 8 ( ,表示 1 个数的和) 。所以前 11 个元素的累加和等于 BITree[8] + BITree[10] + BITree[11] = 21 + 13 + 8 = 42

如果再从更直观的树上看,计算前 11 个元素的累加和,从叶子结点 11 开始,找到 11 的父结点 10,然后找到 10 的父结点 8 ,8 的父结点为 0 ,然后将路径上的值都加起来,就是前 11 个元素的累加和。

不难写出下面计算累加和的代码:

int getSum(int index)
{ int sum = 0; // 累加和// BITree[] 的下标比 arr[] 大 1index = index + 1; // 遍历 BITree[index] 的祖先结点while(index>0) { // 将当前 BITree 的值加到 sumsum += BITree[index]; // 将 index 指向 index 的父结点index = index - index & (-index); } return sum;
}

代码很清晰,就是从给定的 index 遍历 index 的所有的祖先结点,并将遍历到的 BITree[index]  的值加起来即可。

如何将数组中下标为 i 的元素的值更新为 x,且在 O(logn) 的时间内更新树状数组 BITree[] ?

虽然关于这个问题在最开始的时候已有阐述,但我们再以一个例子介绍一遍!

现在将 arr[3] = arr[3] + 6 ,时间复杂度为 :

然后更新树状数组 BITree[] ,时间复杂度为 :

index = 4 ,将 BITree[index] += val ,即 BITree[4] = 7 + 6 = 13 .

更新 indexindex = index + index & (-index) = 4 + 4 = 8 ;

更新 BITree[index] ,即 BITree[8] = 21 + 6 = 27 :

更新 indexindex = index + index & (-index) = 8 + 8 = 16 > 12 ,更新过程结束。

代码也相当简单:

public static void updateBIT(int n, int index, int val)
{ // BITree[] 的下标比 arr[] 大 1index = index + 1; // 遍历所有的祖先,并加上 'val'while(index <= n) { // BIT Tree 的当前结点加上 'val'BITree[index] += val; // 更新 indexindex += index & (-index); }
}

能否将树状数组扩展到以 的时间复杂度计算区间和呢?

答案是肯定的,rangSum(l,r) = getSum(r) - getSum(l - 1) .

复杂度分析

任何一个正整数 n 的二进制表示中置位数的个数为 量级,置位数就是一个整数二进制表示中 1 的数目。因此,getSum()  和 updateBIT() 两个操作至多遍历 个结点。

初始构造树状数组 BITree[] 的时间复杂度为 ,构造 BITree[] 树状数组会调用 updateBIT() 函数 n 次。

完整的实现代码

import java.util.*;
import java.lang.*;
import java.io.*; class BinaryIndexedTree
{ final static int MAX = 100;  static int BITree[] = new int[MAX]; int getSum(int index) { int sum = 0;index = index + 1; while(index>0) { sum += BITree[index]; index -= index & (-index); } return sum; } public static void updateBIT(int n, int index, int val) { index = index + 1; while(index <= n) { BITree[index] += val; index += index & (-index); } } void printBITree(int arr[], int n) {for(int i = 0; i < n; i++) {System.out.print(arr[i] + " ");}System.out.println();}void constructBITree(int arr[], int n) { for(int i=1; i<=n; i++) BITree[i] = 0; for(int i = 0; i < n; i++) {updateBIT(n, i, arr[i]); printBITree(BITree,n+1);}} public static void main(String args[]) { int arr[] = {2, 1, 1, 3, 2, 3, 4, 5, 6, 7, 8, 9}; int n = arr.length; BinaryIndexedTree tree = new BinaryIndexedTree(); // 从给定的数组 arr[], 构造 BITree[]tree.constructBITree(arr, n); System.out.println("arr[0..5] = " + tree.getSum(5)); // 测试更新操作arr[3] += 6; // arr[3] 的改变,更新 BITree[]updateBIT(n, 3, 6);  System.out.println("arr[0..5] = " + tree.getSum(5)); }
}

---

由 五分钟学算法 原班人马打造的公众号:图解面试算法,现已正式上线!
接下来我们将会在该公众号上,为大家分享优质的算法解题思路,坚持每天一篇原创文章的输出,视频动画制作不易,感兴趣的小伙伴可以关注点赞一下哈!

漫画:什么是树状数组?相关推荐

  1. 洛谷 P5057 [CQOI2006]简单题(树状数组)

    嗯... 题目链接:https://www.luogu.org/problem/P5057 首先发现这道题中只有0和1,所以肯定与二进制有关.然后发现这道题需要支持区间更改和单点查询操作,所以首先想到 ...

  2. Color the ball(HDU1556)树状数组

    每次对区间内气球进行一次染色,求n次操作后后所有气球染色次数. 树状数组,上下区间更新都可以,差别不大. 1.对于[x,y]区间,对第x-1位减1,第y位加1,之后向上统计 #include<b ...

  3. 【BZOJ2434】[NOI2011]阿狸的打字机 AC自动机+DFS序+树状数组

    [BZOJ2434][NOI2011]阿狸的打字机 Description 阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机.打字机上只有28个按键,分别印有26个小写英文字母和'B'.'P ...

  4. Codeforces 629D Babaei and Birthday Cake(树状数组优化dp)

    题意: 线段树做法 分析: 因为每次都是在当前位置的前缀区间查询最大值,所以可以直接用树状数组优化.比线段树快了12ms~ 代码: #include<cstdio> #include< ...

  5. poj_3067 树状数组

    题目大意 左右两个竖排,左边竖排有N个点,从上到下依次标记为1,2,...N; 右边竖排有M个点,从上到下依次标记为1,2....M.现在从K条直线分别连接左边一个点和右边一个点,求这K条直线的交点个 ...

  6. hdu 1166 敌兵布阵(树状数组)

    题意:区间和 思路:树状数组 #include<iostream> #include<stdio.h> #include<string.h> using names ...

  7. Equalizing Two Strings 冒泡排序or树状数组

    首先考虑排序后相等 如果排序后相等的话就只考虑reverse长度为2的,所以a或者b排序后存在相邻两个字母相等的话就puts YES,n>26也直接puts YES 不然的话就假设c为a,b排完 ...

  8. Hdu 6534 Chika and Friendly Pairs 莫队算法+树状数组

    题目链接 题意求给区间[L,R]中有少对(i,j)满足i<j且abs(a[i]-a[j])<=k. 首先来说暴力的方法就是离散化,然后用树状数组来维护,但是m次询问,m很大,所以说一定会t ...

  9. HDU - 5877 Weak Pair 2016 ACM/ICPC 大连网络赛 J题 dfs+树状数组+离散化

    题目链接 You are given a rootedrooted tree of NN nodes, labeled from 1 to NN. To the iith node a non-neg ...

最新文章

  1. 简述linux各个组成部分的定义及功能,Mariadb的架构及相关概念
  2. 美团高德并不是解决快车问题的灵药,烧完钱之后只会产生新的滴滴
  3. Python之区块链入门,揭秘比特币
  4. Nutch开发(四)
  5. 如何自动保存邮件草稿
  6. 大道至简,阿里巴巴敏捷教练的电子看板诞生记
  7. 如何在 Web Forms 中引入依赖注入机制
  8. JDK 7的算法和数据结构
  9. 机器学习在本体中的应用研究文献综述
  10. 怎么把html表复制到word里,怎么把网页表格复制到word
  11. spring aop的简单使用
  12. Beetle简单构建TCP服务
  13. Git: The following paths are ignored by one of your .gitignore files: xxx.dll
  14. 咱也来谈谈web打印快递单及经验
  15. 【zer0pts CTF 2022】 Anti-Fermat(p、q生成不当)
  16. HBase 怎样负载均衡?
  17. 编程基础(三十七):PTA运行时错误
  18. 我的世界服务器显示离线,我的世界离线模式怎么玩服务器 | 手游网游页游攻略大全...
  19. java第二作业 手动输入数赋值给数组 求最大值最小值 ,,,,,,输入一个数字 插如数组里 并且保持降序
  20. 【论文翻译】(摘要及引言)The Fourier decomposition method for nonlinear and non-stationary time series analysis

热门文章

  1. Amadeus Pro for Mac v2.8.8 – 多轨音频编辑软件
  2. 双碳时代下,数据中心PUE划红线
  3. EGE 专栏博客写作计划
  4. 手机打开网页显示500服务器错误怎么办,访问网站时出现500错误该如何解决
  5. ORACLE 10G中闪回汇总
  6. Qt界面中的status bar
  7. 最简单的抢购机器人,抢菜,抢nft(很多原理都是类似的)
  8. 统计一个文件英文单词的个数
  9. python入门到精通 练习题30道(初级)
  10. HDLC协议的快速计算方法