1.why 为什么需要树状数组
树状数组可以用来解决 数组区间求和和区间修改的问题。
2.what 什么是树状数组
C为树状数组,A为普通数组
C[1] = A[1];
C[2] = A[1] + A[2];
C[3] = A[3];
C[4] = A[1] + A[2] + A[3] + A[4];
C[5] = A[5];
C[6] = A[5] + A[6];
C[7] = A[7];
C[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8];
how 怎么实现树状数组
- 如何判断树状数组当前位置的值
可以根据上述的规律发现
C[i] = A[i]之前 2k个值相加
C[1 (0001)], k = 0;
C[2 (0010)], k = 1;
C[3 (0011)], k = 0;
C[4 (0100)], k = 2;
将 i 转化为 二进制 发现, k = i的二进制的最后0的数量
1.1 快速求2k,k 为 i的二进制末尾0的个数
2k 其实就是 i 的二进制的最右边的1保留,前面的所有位置为0
思路1: 我自己的思路
i & (i - 1 )可以消除最右边的1,然后用 i 减去该值,可以得到 2k
2k = i - (i & (i - 1))
思路2: 网上通用
2k = i & ( -i );
这个很好理解:
计算机中的负数是补码表示
补码 = 反码 + 1
例如:
原码 1010
反码 0101
补码 0110
除了符号位,负数就是原码保留了最后一位1
所以 2k = i & ( -i );
虽然直观感受思路2步骤较少,更快一点
但是按1-100000000 进行测试,两个思路总耗时基本一致
我们称该算法为 lowbit 算法 采用思路二
public int lowbit(int x) {
// 2^k k 表示 x 后面有多少个0
// 即保留 x 最右边一位1 其余位置0
return x & (-x);
}
lowbit(x) 是一个很特别的值
lowbit(x) 意味则找到了当前节点的子节点数
因为树状数组是二叉树,意味着 lowbit(x) + lowbit(x) 正好是一颗二叉树
x + lowbit(x) = 父节点的下标
x - lowbit(x) = 父亲的左兄弟节点地址
那么,求得了 2k以后,我们需要获得
C[i] = A[i] + A[i-1] + … + A[i - 2k + 1]
共2k个值的和
两种方法实现:
-
循环 A[i] + A[i-1] + … + A[i - 2k + 1]
方法效率:
1-100000000需要遍历 1352152064 O(NlogN) -
利用树状数组实现
C[1] = A[1];
C[2] = C[1] (A[1]) + A[2];
C[3] = A[3];
C[4] = C[2] (A[1] + A[2]) + A[3] + A[4];
C[5] = A[5];
C[6] = C[5] (A[5]) + A[6];
C[7] = A[7];
C[8] = C[4] (A[1] + A[2] + A[3] + A[4]) + C[6] (A[5] + A[6]) + C[7] (A[7]) + A[8];
因为C[8]中包含 C[4].C[6],C[7],而后面的C[16]又会包含C[8],相当于C[16] 也包含C[4],C[6],C[7]
所以,我们可以反过来求和,并不是C[8] = C[4] + C[6] + C[7]
而是 C[4] + C[6] + C[7] = C[8]
具体就是:
判断当前节点属于哪个父亲,给其父亲加上当前节点的值,进行迭代,直到没有父节点
C[parent] = C[i] + lowbit(i);
public void add(int i,int x) {
while(i < size) {
arr[i] += x;
i += lowbit(i+1);
}
}
这样的效率依然是O(NlogN) 但是比遍历效率高很多。
- 区间求和
// [0-i] 的和 [i-j] 的和 = sum(j)-sum(i-1);
public int sum(int i) {
int res = 0;
while(i > 0) {
res += arr[i];
i -= lowbit(i + 1);
}
return res;
}
全部代码如下:
package com.hjh.datastructure;
import java.util.Arrays;
/**
* @author hjh
* @date 2020/2/14
* 树状数组
* 解决单点更新,区间求和问题
* 效率: 修改 logn
* 查询 logn
* 优点: 能解决的问题比线段树快一些
* 缺点: 功能有限
*/
public class TreeArray {
int size = 0;
int[] arr;
int[] original;
public TreeArray(int[] original) {
this.size = original.length;
arr = new int[size];
this.original = original;
build();
}
public void build() {
for(int i = 0 ; i < size ; i++) {
add(i,original[i]);
}
}
public int lowbit(int x) {
// 2^k k 表示 x 后面有多少个0
// 即保留 x 最右边一位1 其余位置0
return x & (-x);
}
public int lowbit2(int x) {
return x - (x & (x - 1));
}
public void add(int i,int x) {
while(i < size) {
arr[i] += x;
i += lowbit(i+1);
}
}
public void update(int i,int x) {
add(i,x - original[i]);
original[i] = x;
}
public int sum(int i) {
int res = 0;
while(i >= 0) {
res += arr[i];
i -= lowbit(i + 1);
}
return res;
}
public int sum(int i,int j) {
return sum(j) - sum(i-1);
}
public void print() {
for(int i = 0 ; i < size ; i++) {
System.out.print(arr[i] + "\t");
}
System.out.println();
}
public static void main(String[] args) {
int [] arr = {1,1,1,1,1,1,1,1};
TreeArray treeArray = new TreeArray(arr);
treeArray.print();
}
}
上述树状数组满足了两个功能:
单点更新 O(logN)
区间求和 O(logN)
训练题:
leetcode 307. 区域和检索 - 数组可修改
单点更新 单点查询
数组
单点更新 区间求和查询
树状数组
区间更新 单点查询
树状数组变种
按照上面的update方法 将i-j的值全部更新一遍,需要执行update j-i次
和遍历没有区别,需要对树状数组做调整,引入差分数组,利用差分数组建树
设数组A,差分数组为D
A = 1,2,10,5,2,1
D = 1,1,8,-5,-3,-1
D[i] = A[i] - A[i-1]
如果对 2-4 的区间增加 4
A = 1,2,14,9,6,1
D = 1,1,12,-5,-3,-5
对于区间更新,差分数组只需要 D[i] + 4; D[j+1] - 4; 即可完成区间更新
利用差分数组建树
public class TreeArray2 {
int size = 0;
int[] arr;
public TreeArray2(int[] original) {
this.size = original.length;
arr = new int[size];
add(0,original[0]);
for(int i = 1 ; i < size ; i++) {
add(i,original[i] - original[i-1]);
}
}
public int lowbit(int x) {
return x & (-x);
}
public void add(int i,int x) {
while(i < size) {
arr[i] += x;
i += lowbit(i+1);
}
}
public void update(int i,int j,int x) {
add(i,x);
add(j+1,-x);
}
public int getVal(int i) {
int res = 0;
while(i >= 0) {
res += arr[i];
i -= lowbit(i + 1);
}
return res;
}
public void print() {
for(int i = 0 ; i < size ; i++) {
System.out.print(getVal(i) + "\t");
}
System.out.println();
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7};
TreeArray2 tr2 = new TreeArray2(arr);
tr2.print();
tr2.update(3,4,10);
tr2.print();
}
}
利用差分数组构建树状数组
sum转化为 getVal();
update只更新头尾两个差值即可
区间更新 区间求和查询
∑ni = 1A[i] = ∑ni = 1 ∑ij = 1D[j];
则A[1]+A[2]+…+A[n]
= (D[1]) + (D[1]+D[2]) + … + (D[1]+D[2]+…+D[n])
= nD[1] + (n-1)D[2] +… +D[n]
= n * (D[1]+D[2]+…+D[n]) - (0D[1]+1D[2]+…+(n-1)*D[n])
所以上式可以变为
∑ni = 1A[i] = n*∑ni = 1D[i] - ∑ni = 1( D[i]*(i-1) );
如果你理解前面的都比较轻松的话,这里也就知道要干嘛了,维护两个数状数组,sum1[i] = D[i],sum2[i] = D[i]*(i-1);支持 单点更新,单点查询,区间增加,区间求和
package com.hjh.datastructure;
/**
* @author hjh
* @date 2020/2/14
* 树状数组
* 解决区间更新,区间查询问题
* 效率: 修改 logn
* 查询 logn
*
* 需要两个树状数组
*/
public class TreeArray3 {
int size = 0;
int[] arr;
int[] arr2;
public TreeArray3(int[] original) {
this.size = original.length;
arr = new int[size];
arr2 = new int[size];
add(0,original[0]);
for(int i = 1 ; i < size ; i++) {
add(i,original[i] - original[i-1]);
}
}
public int lowbit(int x) {
return x & (-x);
}
// 单点增加
public void add(int i,int x) {
int pos = i;
while(i < size) {
arr[i] += x;
arr2[i] += x * (pos - 1);
i += lowbit(i+1);
}
}
// 单点更新
public void updateVal(int i , int x) {
add(i,i,x - getVal(i));
}
// 区域增加
public void add(int i,int j,int x) {
add(i,x);
add(j+1,-x);
}
// 单点查询
public int getVal(int i) {
int res = 0;
while(i >= 0) {
res += arr[i];
i -= lowbit(i + 1);
}
return res;
}
// 区域求和
public int sum(int i) {
int res = 0, x = i;
while(i > 0){
res += x * arr[i] - arr2[i];
i -= lowbit(i+1);
}
return res;
}
// 区域求和
public int sum(int i, int j) {
return sum(j) - sum(i-1);
}
public void print() {
for(int i = 0 ; i < size ; i++) {
System.out.print(getVal(i) + "\t");
}
System.out.println();
}
public static void main(String[] args) {
int[] arr = {1,2,3,4,5,6,7};
TreeArray3 tr2 = new TreeArray3(arr);
tr2.print();
tr2.add(3,4,10);
tr2.print();
System.out.println(tr2.sum(3,4));
}
}
###写在最后
树状数组因为较方便实现,所以在日常用的较多
但是,同样的功能,线段树可以完全满足,
而线段树支持的功能,树状数组不一定满足。
所以学习树状数组的同时也需要学习线段树,还有线段树变种
ZKW线段树