js 数组 实现 完全树_算法和数据结构 | 树状数组(Binary Indexed Tree)

ccc38bebb09e46401da87808c3ce70a1.png

本文来源于力扣圈子,作者:胡小旭。点击查看原文

力扣​leetcode-cn.com

树状数组或二叉索引树(英语:Binary Indexed Tree),又以其发明者命名为 Fenwick 树。其初衷是解决数据压缩里的累积频率(Cumulative Frequency)的计算问题,现多用于高效计算数列的前缀和, 区间和。它可以以

的时间得到任意前缀和,并同时支持在
时间内支持动态单点值的修改。空间复杂度

文章先介绍低位运算(lowbit)的基本知识,再提及如何将一个整数划分为

个区间的运算过程,进而延展到如何将线性序列以树行结构进行存取,接着介绍了高级数据结构——树状数组的两个基本操作——查询前缀和与单点增加,最后介绍了树状数组的一个应用——求解逆序对数。

lowbit(低位)运算

定义为非负整数
在二进制表示下 “最低位的 1 及其后边所有的 0” 构成的数值。

比如:

,其二进制表示为
,则其低位

公式

如何计算一个整数

中二进制表示下所有位是 1 的数值?

比如

,则其二进制表示下所有位是 1 的数值有:

朴素算法需要枚举整数中所有的位,时间复杂度为

为整数
的二进制表示下的位数。

为了高效获取二进制表示下所有位是 1 的数值,可以利用

运算,得到时间复杂度
为二进制表示下为 1 的位的个数。

比如

;接着令
,则
;接着令
,停止。

为了得到

的第几位为 1,可以对 2 和 8 分别取对数,即
。由于 C++ math.h 库的
函数是以
为底的实数运算,并且复杂度常数较大,所以可以通过预处理,利用哈希表来代替
运算。

代码

C++ 实现

const MAX_N = 1 << 20;
int H[MAX_N + 1];
for (int i = 0; i <= 20; ++i) H[1 << i] = i;
while (cin >> n) {
    while (n > 0) {
        cout << H[n & -n] << ' ';
        n -= n & -n;
    }
    cout << endl;
}

树状数组

假设整数

,其二进制表示形式为:

代表二进制表示下位为 1 的索引下标值,且假设

那么,可以将区间 [1,n] 划分成

个小区间。
  • ...

比如,

,那么
区间可以划分成
,其区间长度分别为

利用

运算计算区间:

C++ 实现

while (x > 0) {
    printf("[%d, %d]n" x - lowbit(x) + 1, x);
    x -= lowbit(x);
}

树状数组是基于以上思想的数据结构,基本用途是维护序列的前缀和。

那么,假设有序列

,现在的问题就是如何将这个序列划分成
个小区间。不妨,利用序列的索引值(以 1 为起点开始计数),根据上述计算区间的方式,将其以如下树形结构展开。

75d9c6bb4bfb466ccd6071b52d8cc157.gif
树状数组(Binary Indexed Tree) 以树形结构展开的序列 A

此时,以树形结构展开的序列 A 中的每一个节点都对应着树状数组中的一个值。那么这个值为以当前节点为根的子树中所有节点值的总和。

接着,我们看下以树形结构展开的树状数组是什么样的。

1aede75267ab5d1ed6fb70ab14bcb639.gif
以树形结构展开的树状数组(Binary Indexed Tree)
  • Index 代表序列 A 中元素的索引,为了方便,以 1 为起点计数
  • Original Value 代表序列 A 中的元素值
  • BIT Value(Binary Indexed Tree Value)代表树状数组中的值
  • Binary bit 代表索引值的二进制形式
  • Low bit 代表索引值的二进制形式下的地位

上图中最大的区别是某些节点中的值发生了变化。这是因为,在以树形结构展开的树状数组中的每一个值代表的是一个区间的总和。这个区间即为我们上述求解的区间,比如一个整数 7,可以将其划分成

三个小区间。那么,这三个小区间的右端值作为索引对应的树状数组中的值即为当前区间元素的总和。

比如

对应的树状数组的值为(BIT Value)10,它代表
这个区间的和。

再比如

对应的树状数组的值为 11,它代表
这个区间的和。

基本操作

树状数组支持两个基本操作——查询前缀和,单点增加。

查询前缀和

在寻求序列 A 的前 n 项的前缀和时,等于

代表的
个区间的总和。

C++ 实现

int query(int x) {
    int ans = 0;
    for (; x; x -= x & -x) ans += bit[x];
    return ans;
}

单点增加

观察父子节点的关系,可以推算出,父节点的索引 parent(i),为其子节点索引值 + 其低位——

.

C++ 实现

void update(int x, int delta) {
    for (; x; x += x & -x) bit[x] += delta;
}

关于查询前缀和与单点增加的计算过程,可以观看下面视频展示的动画。

d9eb33f49eef8cd5dc5751e5149281ed.gif

树状数组与逆序对

对于一个序列

,如果
,并且
,那么则称
构成逆序对。利用树状数组数据结构可以求解序列
中的逆序对个数。
  1. 逆序遍历序列
  2. 利用树状数组的性质,使用
    操作获取每一个元素的逆序对数
  3. 将当前元素更新
    到树状数组中
  4. 循环迭代上述步骤,直到遍历所有元素

C++ 实现

int cnt = 0;
for (int i = A.size() - 1; i >= 0; --i) {
    cnt += query(A[i]);
    update(A[i], 1);
}

在每一次更新

树状数组时,以元素的值作为树状数组的索引,更新的值为 +1,代表个数。

在每一次获取

逆序对数时,存在于树状数组中的元素的索引值都比当前元素的大(逆序遍历),那么自然获取到的树状数组的值即为索引值比当前元素的大,且值比当前元素的小的个数。

注意,上述的求解过程时,如果序列A的值范围较大时,那么需要离散化处理。

参考

  • 《算法竞赛进阶指南》
  • 维基百科——树状数组

本文作者:胡小旭

声明:本文归作者版权所有,如需转载请联系。文中图片和视频为作者“胡小旭”制作,未经允许严禁修改和翻版使用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值