数据结构-树状数组

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 怎么实现树状数组

  1. 如何判断树状数组当前位置的值
    可以根据上述的规律发现
    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个值的和

两种方法实现:

  1. 循环 A[i] + A[i-1] + … + A[i - 2k + 1]
    方法效率:
    1-100000000需要遍历 1352152064 O(NlogN)

  2. 利用树状数组实现

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) 但是比遍历效率高很多。

  1. 区间求和
	// [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 = 1ij = 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]) - (0
D[1]+1
D[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线段树

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
树状数组是一种用于加速前缀和操作的数据结构。它可以在O(logn)的时间复杂度内更新单个元素,并且可以在O(logn)的时间复杂度内查询一个区间的和。\[1\] 树状数组的基本思想是将数组分解成若干个长度为2的幂次的区间,每个区间的和都可以通过一系列区间和的累加得到。树状数组的每个节点都存储了一段区间的和,通过不断迭代lowbit()运算,可以得到从1到x之间的和。\[3\] 在树状数组的实现中,可以使用add()函数来更新单个元素的值,使用query()函数来查询一个区间的和。add()函数通过迭代lowbit()运算,将更新的值加到对应的节点上。query()函数通过计算两个前缀和的差值来得到一个区间的和。\[2\] 差分树状数组树状数组的一种变体,它可以用来求解区间最大值。差分树状数组的基本思想是将原始数组转化为差分数组,然后对差分数组建立树状数组。通过查询树状数组得到的前缀和,再加上差分数组的前缀和,就可以得到原始数组的区间最大值。\[2\] 综上所述,树状数组是一种用于加速前缀和操作的数据结构,可以在O(logn)的时间复杂度内更新单个元素和查询一个区间的和。差分树状数组树状数组的一种变体,用于求解区间最大值。\[1\]\[2\]\[3\] #### 引用[.reference_title] - *1* *2* *3* [RMQ问题--------树状数组](https://blog.csdn.net/weixin_43743711/article/details/107191842)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值