树状数组(一)


前言

给定一段数字,需要求前缀和和修改其中的数字,使用数组进行前缀和初始化和暴力修改在数据量很大的情况下已经不满足我们的需求,此时便需要树状数组来帮助我们解决问题。那么什么是树状数组呢,本篇文章将通过概念+实战的方式,让读者了解树状数组的概念与用法。


一、树状数组简介

树状数组或二叉索引树(英语:Binary Indexed Tree),又以其发明者命名为Fenwick树。现多用于高效计算数列的前缀和, 区间和。

二、树状数组的原理与相应模块

例子引入:假如我们需要计算数字1、5、7、9、3、4、5、8的前缀和,区间和并且修改其中的元素,我们该如何利用树状数组来解决问题呢?

先看一幅图片:
在这里插入图片描述
如图所示,我们可以利用最上面的数组来对这8个数字来进行所需要的操作,为什么是这8个数字,首先我们要知晓这8个数字怎么来的。
(1) 我们要知晓数字1、5的和,我们直接可以通过1+5得到。这是暴力的做法,在树状数组中我们可以通过22直接得到。

(2) 如果想要得到1、5、7、9的和,我们可以根据1+5+7+9得到,也可以根据6+16得到,那么我们肯定选择6+16,因为操作的数很少。当然在树状数组中,我们自然可以通过22。

(3) 那我们想要得到1+5+7呢,根据树状数组,我们可以通过6+7来得到。这个时候我们会发现一个规律,在树状数组下面的每一行,偶数的矩形不被我们需要,去掉之后正好剩余8个矩形,组成我们的树状数组。

通过上述所描述,我们可以通过这8个数组成的树状数组得到任意的区间和。那么首先我们要知晓,如何用这8个数字建立树状数组呢。

我们观察树状数组,第一个元素在数组下标为1的位置(不是下标为0)。而1的二进制形式为1。那么我们可以很容易知晓1的二进制最低位为第一位,即该树状数组元素包含了a[1]计算出来的。因为二进制中只有一位,所以该树状数组的第一个元素即为原来数组中的一个元素。

那么树状数组中的3#元素呢,3的二进制最低位为1,所以存储的即为长度为1的元素,即为原来数组中的7。

有了上面的性质,我们就来尝试如何从原来的数组建立树状数组。遍历原来的数组nums,得下标为i(从0开始一直到7)。
那么我们称建立的树状数组为Fenwick[10]。
建立过程为

(1) 首先我们要知道一个lowbit函数,用来求一个数字的最低位的。

int lowbit(int x){
    return x&(-x);
}

探论其原理,设一个数的八位二进制位X XXXXXXX
5的二进制形式为 0 0000101
-5是5进行按位取反+1 即为 1 1111011
则进行位运算则为1,即为最位了。

(2) 接着进行建树过程

void build(vector<int> &nums, vector<int> &Fenwick){
	for(int i = 0; i < nums.size(); ++i){
		int index = i+1;
		while(index <= 8){
			Fenwick[index] += nums[i];
			index += lowbit(index); 
		}
	}
}

(3) 那我们我们怎么求出前x个元素的和呢。
还是回到例子中,我们想要前7个元素,取得的是树状数组中的7#,6#,4#。即为7依次减去二进制最低位的结果。
7 - 0 = 7;
7 - 1 = 6;
6 - 2 = 4;
4 - 4 = 0;

int calculate(vector<int> &Fenwick, int k){
	int ret = 0;
	while(k > 0){
		ret += Fenwick[k];
		k -= lowbit(k);
	}
return ret;
}

(4) 如果第几个元素修改了,参考建树过程对相应的树状数组中的元素进行计算即可。

(5) 如果求的是第4个到第7个元素之和,相当于求的是前7个元素和 减去 前3个元素之和。

三、实战演练

3.1 区域和检索 - 数组可修改

3.1.1 题目链接

点击跳转到题目位置

3.1.2 题目描述

给你一个数组 nums ,请你完成两类查询。

其中一类查询要求 更新 数组 nums 下标对应的值
另一类查询要求返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的 ,其中 left <= right
实现 NumArray 类:

  • NumArray(int[] nums) 用整数数组 nums 初始化对象
  • void update(int index, int val) 将 nums[index] 的值 更新 为 val
  • int sumRange(int left, int right) 返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的 (即,nums[left] + nums[left + 1], …, nums[right])

3.1.3 题目代码

class NumArray {
    vector<int> Fenwick;
    vector<int> num;
    int n ;
    int lowbit(int x){
        return x&(-x);
    }

public:
    NumArray(vector<int>& nums) {
        num = nums;
        n = nums.size();
        Fenwick.resize(n+1);
        for(int i = 0; i < n; ++i){
            int index = i + 1;
            while(index <= n){
                Fenwick[index] += nums[i];
                index += lowbit(index);
            }
        }
    }
    
    void update(int index, int val) {
        int change = (val - num[index]);
        num[index] = val; 
        index++;
        while(index <= n){
            Fenwick[index] += change; 
            index += lowbit(index); 
        }
    }
    
    int calculate(vector<int> &Fenwick, int k){
	    int ret = 0;
	    while(k > 0){
		    ret += Fenwick[k];
		    k -= lowbit(k);
	    }
    return ret;
    }

    int sumRange(int left, int right) {
        return calculate(Fenwick, right+1) - calculate(Fenwick, left);    
    }
};

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray* obj = new NumArray(nums);
 * obj->update(index,val);
 * int param_2 = obj->sumRange(left,right);
 */

3.1.4 解题思路

(1) 这是一道典型的树状数组的模板题,与概念中所指出的背景相同。所以只需要完整套用树状数组的模板即可。

3.2 数字流的秩

3.2.1 题目链接

点击跳转到题目位置

3.2.2 题目描述

假设你正在读取一串整数。每隔一段时间,你希望能找出数字 x 的秩(小于或等于 x 的值的个数)。请实现数据结构和算法来支持这些操作,也就是说:

实现 track(int x) 方法,每读入一个数字都会调用该方法;

实现 getRankOfNumber(int x) 方法,返回小于或等于 x 的值的个数。

注意:本题相对原题稍作改动

3.2.3 题目代码

class StreamRank {
    vector<int> Fenwick;
public:
    int lowbit(int x){
        return x&(-x);
    }

    StreamRank() {
        Fenwick.resize(50010);
    }
    
    void track(int x) {
        x++;
        while(x <= 50001){
            Fenwick[x]++;
            x += lowbit(x); 
        }
    return ;
    }
    
    int getRankOfNumber(int x) {
        int ans = 0;
        x++;
        while(x > 0){
            ans += Fenwick[x];
            x -= lowbit(x);
        }
        return ans;
    }
};

/**
 * Your StreamRank object will be instantiated and called as such:
 * StreamRank* obj = new StreamRank();
 * obj->track(x);
 * int param_2 = obj->getRankOfNumber(x);
 */

3.2.4 解题思路

(1) 这道题目也可以作为树状数组的模板题,只不过与前面概念中所说的有变化。概念中数组中的数换成了0~50000,即在梳妆数组中要安排下标1到50001的位置。原来树状数组所求的前缀和变成了小于x的元素有多少个。

(2) 每次插入数字x即在树状数组中的相应位置增加一个原来数组中下标为x+1的元素。

(3) 要找树状数组中小于数字x的元素,即求前面0到x-1数字有多少个。即套用原来求前缀和的公式即可。


总结

相信读者读完了,对于树状数组有了相应的理解,当读者尝试去完成LeetCode上树状数组的两道例题,便能对树状数组的实际应用有了一个深刻的理解。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值