漫画:什么是树状数组?
我们学习数据结构的目的在于将我们的算法变得更快。由 Peter M. Fenwick 提出的树状数组 BIT 结构就是一个优秀的数据结构,BIT 全称 Binary Indexed Trees 结构,而不是所说的比特奥。Peter M. Fenwick 首次使用此结构进行数据压缩。在算法竞赛中,通常用于存储频率和处理累积频率表。
首先考虑一个简单的问题。
给定一个数组 arr[0 ... n-1]
,如何实现下面两个操作:
计算前 i 个元素的累加和;
将数组中下标为 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[]
呢?
答案很简单:
首先将树状数组
BITree[]
的所有元素初始化为 0;调用
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 = 16
,16 > 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]
:
树状数组的几大特点:
BITree[0] 是一个虚拟结点,同时也是我们所看到的根结点
BITree[y]
是BITree[x]
的父结点,当且仅当 y 可以通过从 x 的二进制表示中删除最后一个位置的 1 (也就是从右向左第一个) 来获得,即y = x - (x & (-x))
BITree[y]
的孩子结点BITree[x]
存储的是数组arr[]
中下标从 y (包含) 到 x (不包含) 的累加和,即arr[y,...,x)
,注意括号是不包含 x;
关于这个第三条可能需要稍微解释一下:
BITree[8]
的孩子结点 BITree[12]
的值等于 30 ,表示数组 arr[] 中下标从 8 到 12(不包含 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
.
更新 index
,index = index + index & (-index) = 4 + 4 = 8
;
更新 BITree[index]
,即 BITree[8] = 21 + 6 = 27
:
更新 index
,index = 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)); }
}
---
由 五分钟学算法 原班人马打造的公众号:图解面试算法,现已正式上线!
接下来我们将会在该公众号上,为大家分享优质的算法解题思路,坚持每天一篇原创文章的输出,视频动画制作不易,感兴趣的小伙伴可以关注点赞一下哈!
漫画:什么是树状数组?相关推荐
- 洛谷 P5057 [CQOI2006]简单题(树状数组)
嗯... 题目链接:https://www.luogu.org/problem/P5057 首先发现这道题中只有0和1,所以肯定与二进制有关.然后发现这道题需要支持区间更改和单点查询操作,所以首先想到 ...
- Color the ball(HDU1556)树状数组
每次对区间内气球进行一次染色,求n次操作后后所有气球染色次数. 树状数组,上下区间更新都可以,差别不大. 1.对于[x,y]区间,对第x-1位减1,第y位加1,之后向上统计 #include<b ...
- 【BZOJ2434】[NOI2011]阿狸的打字机 AC自动机+DFS序+树状数组
[BZOJ2434][NOI2011]阿狸的打字机 Description 阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机.打字机上只有28个按键,分别印有26个小写英文字母和'B'.'P ...
- Codeforces 629D Babaei and Birthday Cake(树状数组优化dp)
题意: 线段树做法 分析: 因为每次都是在当前位置的前缀区间查询最大值,所以可以直接用树状数组优化.比线段树快了12ms~ 代码: #include<cstdio> #include< ...
- poj_3067 树状数组
题目大意 左右两个竖排,左边竖排有N个点,从上到下依次标记为1,2,...N; 右边竖排有M个点,从上到下依次标记为1,2....M.现在从K条直线分别连接左边一个点和右边一个点,求这K条直线的交点个 ...
- hdu 1166 敌兵布阵(树状数组)
题意:区间和 思路:树状数组 #include<iostream> #include<stdio.h> #include<string.h> using names ...
- Equalizing Two Strings 冒泡排序or树状数组
首先考虑排序后相等 如果排序后相等的话就只考虑reverse长度为2的,所以a或者b排序后存在相邻两个字母相等的话就puts YES,n>26也直接puts YES 不然的话就假设c为a,b排完 ...
- Hdu 6534 Chika and Friendly Pairs 莫队算法+树状数组
题目链接 题意求给区间[L,R]中有少对(i,j)满足i<j且abs(a[i]-a[j])<=k. 首先来说暴力的方法就是离散化,然后用树状数组来维护,但是m次询问,m很大,所以说一定会t ...
- 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 ...
最新文章
- 简述linux各个组成部分的定义及功能,Mariadb的架构及相关概念
- 美团高德并不是解决快车问题的灵药,烧完钱之后只会产生新的滴滴
- Python之区块链入门,揭秘比特币
- Nutch开发(四)
- 如何自动保存邮件草稿
- 大道至简,阿里巴巴敏捷教练的电子看板诞生记
- 如何在 Web Forms 中引入依赖注入机制
- JDK 7的算法和数据结构
- 机器学习在本体中的应用研究文献综述
- 怎么把html表复制到word里,怎么把网页表格复制到word
- spring aop的简单使用
- Beetle简单构建TCP服务
- Git: The following paths are ignored by one of your .gitignore files: xxx.dll
- 咱也来谈谈web打印快递单及经验
- 【zer0pts CTF 2022】 Anti-Fermat(p、q生成不当)
- HBase 怎样负载均衡?
- 编程基础(三十七):PTA运行时错误
- 我的世界服务器显示离线,我的世界离线模式怎么玩服务器 | 手游网游页游攻略大全...
- java第二作业 手动输入数赋值给数组 求最大值最小值 ,,,,,,输入一个数字 插如数组里 并且保持降序
- 【论文翻译】(摘要及引言)The Fourier decomposition method for nonlinear and non-stationary time series analysis