树状数组---快速上手

树状数组的是一种动态维护数组区间和的数据结构。

1.树状数组和线段树有什么区别?

试想一个场景:

有一个长度为 n 的数组: [a0,a1,a2,a3,....a4]
现在有两种操作: 1.修改数组某个元素的值
               2.获取数组某个区间的所有元素的和(即区间和)。

这种场景下,使用线段树是完全可以做到的。回顾:线段树的作用:对给定的一个长度为 n 的数组 nums[ 0 ..... n - 1]而言 ,线段树能够用来存储数组的某一连续子数组 nums[ left , right]的特征(可以是区间和、区间最大值、区间最小值)。在本场景下,特征就是区间和。

例如,对于数组:[8,6,1,4,5,5,1,1,3,2,1,4,9,0,7,4],n = 16,线段树数组的树状结构是这样子的:

一维数组是这个样子的:

但如果用树状数组,则数组空间是这样样子的:树状数组的长度等于原数组长度 N

【结论】

也就是说,在解决【区间和】问题上,树状数组比线段树数组要节省至少一倍的空间!!!查找/修改操作的性能要优于线段树数组。

树状数组只能解决区间和问题,而线段树数组可以解决一系列的区间问题,比如区间和,区间最大值、最小值问题等。

2.树状数组的定义

定义:对于任意一个数组 a[1....n]而言(放弃数组的第0个位置),我们可以构造它的树状数组 c[1.....n]

让数组 a 的前缀和 sum[a1 + a2 + ... + ax],即 prex[x],满足:

x 被二进制分解为:

$$
\Large x = 2^{i_1} + 2^{i_2}+2^{i_3}+...+2^{i_m} ,其中 i_1 > i_2 > i_3 ...
$$

 

$$
\large prex[x] = c[2^{i_1}] + c[2^{i_1} + 2^{i_2}] + c[2^{i_1} + 2^{i_2} + 2^{i_3}] +... + c[x]
$$

例如, prex[13] = prex[(1101)] = perx[8 + 4 + 1] = c[8] + c[8+4] + c[8+4+1] = c[8] + c[12] + c[13]

有了这个定义,我们可以借助图来理解树状数组的各个元素之间的关系:(父节点值等于子节点值之和)

例如,c[9] = a[9], c[8] = c[4] + c[6] + c[7] + a[8], c[2] = c[1] + a[2],等等

 3.树状数组单点修改

已知树状数组c原数组a,当修改了原数组a的某个元素时,该如何调整数组c中的值以维持正确的树状数组呢?

【思考】

假设现需要将 a[x] (1 <= x <= n) 的值修改为 newValue,则等价于 往 a[x] 加上 t (t = newValue - a[x])

这时,我们需要做的,就是让该节点的直接父节点以及间接父节点的值都加上 t 即可。例如,往 a[5]上加了1,那就需要往 c[5]、c[6]、c[8] 上都加1.

【关键】

那我们又如何通过叶节点(即原数组节点),依次找到它的祖先节点呢?

$$
\large c[x] \quad 的父节点是 \quad c[x+ lowbit(x)]
$$

其中,lowbit(x) 的定义是:

$$
\large x = 2^{i_1} + 2^{i_2}+2^{i_3}+...+2^{i_m} ,其中 i_1 > i_2 > i_3 ... \\ \large lowbit(x) = 2^{i_m}
$$

例如,当x = 9,二进制分解: x = 8 + 1,则 C9的父节点是 c[9 + 1] = c[10]

例如,当 x = 12,二进制分解: x = 8 + 4,则c12的父节点是 c[12 + 4] = c[16]

【如何实现lowbit()】

 * 如何求 lowBit(12)呢? 12的二进制表示为 0000 1100 (原码),如何得到最低位的为1的位是第几位呢?
 * 我们要的是从低位开始,第一个1保持不变,而其他位都变为 0 (正数的原码的符号位也是0)
 * 做法就是: 对原码按位取反后,加一,然后将结果与原码进行与运算。
 * 例如, (0000 1100)取反 ->   11110011  加一  --> 11110100
 *    11110100 & 00001100 -> 00000100 = 4
 
     public int lowBit(int x){
        return x & -x; //对原码取反+1,得到的正是该数的负数的二进制存储格式,所以这里直接写 -x
    }

4.树状数组的单点查询

已知树状数组 c[1.....n],如何计算 prex[x]? 其中 1 <=x <= n

很简单,直接根据定义,将 x 进行二进制分解,得到数组c的一些元素的下标,然后相加这些元素即可。还是放定义中的例子:

prex[13] = prex[(1101)] = perx[8 + 4 + 1] = c[8] + c[8+4] + c[8+4+1] = c[8] + c[12] + c[13]

在编码上,我们可以用 lowbit来简化运算吗?是可以的!我们可以从x开始往前数,每次间隔lowbit的最小位。

当x = 13, c[13]肯定包括。
lowbit(13) = lowbit(1101) = 1, 所以 c[13-1] = c[12]也包括。
lowbit(12) = lowbit(1100) = 4,所以,c[12-4] = c[8]也包括。
lowbit(8) = lowbit(1000) = 8,c[8-8] = c[0],运算结束。 

 5.树状数组如何初始化?

如何根据原数组 a , 来初始化树状数组c呢?

【思路】

将初始化的过程等价于:对空数组 a 进行 n 次单点修改,同时更新树状数组。

 6.练习与代码实现

307. 区域和检索 - 数组可修改icon-default.png?t=N7T8https://leetcode.cn/problems/range-sum-query-mutable/

【代码】

/**
 * 采用树状数组解法
 *
 */
public class NumArray2 {
    private int[] a;//原数组
    private int[] c;//树状数组
    private int n;
    public NumArray2(int[] nums) {
        n = nums.length;
        a = new int[n+1];
        c = new int[n+1];
        for (int i = 0; i < n; i++){
            add(i+1,nums[i]);
        }
    }

    /**
     * 单点修改
     * @param x 要修改的位置, 1<=x <=n
     * @param t 增量
     */
    public void add(int x,int t){
        a[x] += t;
        int i = x;
        while (i <= n){
            c[i] += t;
            i += lowBit(i);
        }
    }

    /**
     * 单点查询  1<=x <=n
     * @param x
     * @return 返回前缀和
     */
    public int query(int x){
        int sum = 0;
        while (x > 0) {
            sum += c[x];
            x -= lowBit(x);
        }
        return sum;
    }

    public int lowBit(int x){
        return x & -x; //对原码取反+1,得到的正是该数的负数的二进制存储格式,所以这里直接写 -x
    }

    public void update(int index, int val) {
        int t = val - a[index + 1];
        add(index + 1,t);
        a[index + 1] = val;
    }

    public int sumRange(int left, int right) {
        return query(right + 1) - query(left);
    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值