引入问题:力扣题目
给出一个长度为n的数组,完成以下两种操作多次:1、将下标为index的元素的值加上k(k可以是正数也可以是负数) 2、输出区间[left, right]内所有元素的和(包含边界)
朴素算法:对区间的元素累加求和,复杂度O(n^2),单点修改复杂度O(1),区间查询复杂度O(n)
前缀和:复杂度O(n^2),单点修改复杂度O(n),因为修改完数组值后还要修改前缀树组的值,区间查询复杂度O(1)
可以看出以上两种方法时间复杂度都比较高,而用树状数组可以有效的降低时间复杂度。
树状数组:树状数组或二叉索引树(英语:Binary Indexed Tree),又以其发明者命名为Fenwick树,最早由Peter M. Fenwick于1994年以A New Data Structure for Cumulative Frequency Tables为题发表在SOFTWARE PRACTICE AND EXPERIENCE。其初衷是解决数据压缩里的累积频率(Cumulative Frequency)的计算问题,现多用于高效计算数列的前缀和, 区间和。(复杂度O(nlogn),单点修改复杂度O(logn),区间查询复杂度O(logn)).
前置知识:lowbit函数
lowbit函数即求一个非负整数在二进制表示下最低位1和后面的0所构成的十进制值。如lowbit(18) = 2(18的二进制表示为0b10010, 0b10的十进制值为2),lowbit(32) = 32(32的二进制表示为0b100000,0b100000的十进制值为32)......
lowbit的实现方法有两种:
1、消去最后一位的1然后再用原数字减去消去最后一位1的数。
int lowbit(int n){
return n - (n & (n - 1));
}
/*
因为非负整数数n可以表示为x100...0,则n - 1可以表示为x011...1
其中x为n最后一位1前面的位,设最后一位1后面跟了m个0,则n - 1后最后一位1变为0,
后面的m个0变为1,两者做与运算后的值为x000...0,x后跟了m + 1个0,即最后一位1
被消掉了。
eg:0b1001101000 - 1 = 0b1001100111
0b1001101000 & 0b1001100111 = 0b1001100000 最后一位1被消除了
所以 n & (n - 1)可以消除n的最后一位1
*/
2、将一个正整数与他的负数相与
int lowbit(int n){
return n & (-n);
}
/*
对n的每一位取反再加1即可得到-n, 同样找到n的最后一位1,把最后一位1左边的位按位取反,
右边的位不变也可以求得-n,这是在学补码时学过的知识,所以n & (-n)可以获取lowbit值
*/
对于一个数组a,我们可以在其上建立一个这样的树状数组t:
该结构的性质:
- t[x]保存以x为根的子树中叶子结点的和。
- t[x]结点覆盖的长度等于lowbit(x),即t[x]表示的是从a[x] ~~ a[x - lowbit[x] + 1]之间所有值的和,包括端点,观察上图可知同一层的lowbit值都相同,所以同层的t[x]覆盖长度相同
- t[x]结点的父结点为t[x + lowbit(x)]
- 整棵树的高度为(logn) + 1
对树状数组的操作:
1、add(index, k)操作,将数组a的下标为index的元素加上k
从子节点开始一层一层向上更改沿途的结点值。
void add(int index, int k){
for(; index <= n; index += lowbit(index))t[index] += k;//n为根节点值
}
2、ask(int x)操作,返回下标x即前所有元素的和(前缀和)
查询一个点的前缀和,需要从该点向左上不停的找上一个结点相加,而该结点减去lowbit恰好等于左上相邻结点。
int ask(int x){
int ans = 0;
for(; x != 0; x -= lowbit(x))ans += t[x];
return ans;
}
若要求区间和,可以求出前缀和并相减。
3、built_tree()树状数组的创建
//给定数组nums,创建树状数组tree,nums是下标从0开始的数组
void built_tree(){
int n = nums.size();
//注意确保在进行迭代操作前tree中的所有元素值均为0
for(int i = 0; i < n; i++)add(i + 1, nums[i]);
}
图片来源:〔manim | 算法 | 数据结构〕 完全理解并深入应用树状数组 | 支持多种动态维护区间操作_哔哩哔哩_bilibili