树状数组,顾名思义,树的形状的数组,最终的数据结构还是一个数组
作用
树状数组的作用主要是用于对区间的查询,比如区间和,区间更新。
那么有人可能会问,求区间为什么不用前缀和,那不是更简单吗?
- 求和
在求和的时候前缀和跟树状数组是一样的甚至比树状数组更方便 - 更新
但是如果我们要更新这个数组呢,对于前缀和是不是平均复杂度是O(N)?树状数组在这个时候,可以做到O(logN),这一点树状数组有很大优势
所以我们最主要的就是,树状数组是怎么让一个数组可以做到求区间和跟更新都做到O(logN)复杂度的。
基本思想 - 二进制
首先,我们要知道,树状数组能有这么好的效果,最基本的思想是根据二进制来实现的,具体怎么实现的,接下来详讲。
1.将前缀和改成管理区间
对于更新数组最重要的一个问题就是对于前缀和,每个元素代表的数是从自身开始到前面所有的元素和,这样虽然方便了求区间结果,但是在根本上,将更新变成了O(N)的复杂度。
所以树状数组的第一步就是,怎么改变前缀和,但是又能让他起到前缀和的作用呢?我们来看看他是怎么做的。
将前缀和改成管理区间到了这里就要介绍树状数组的关键函数lowbit了,这个函数将贯穿树状数组操作的始终,非常重要
1.1lowbit函数
lowbit(x)得到的是:从低位到高位数,第一个数字1的位置得到的二进制数
比如8=1000,lowbit(8) = 8 2=10 , lowbit(2) = 2, 5 = 110 lowbit(6) = 2
既然这一步是树状数组中最常用的一步,那么树状数组的性能就跟这一步息息相关,所以lowbit的实现非常简单
lowbit(x) = x & (-x)
所以说,大道至简啊,看上去非常简单的一个函数表达式,但是能完美解决我们的问题。
其实这个方法是用到了计算机底层计算用到的是补码的机制。
原码求补码有一个小技巧,那就是除去符号位,从右往左第一个1的下一位开始,所有位直接取反,这个不知道的可以去好好复习一下补码
从右往左第一位,看到这个是不是突然发现有点意思了。一个是原码,一个是补码,如果两个数&一下的话,补码跟原码有相同位的,是不是只有从右往左的第一个1?那么求与的话就肯定能得到我们想要的答案了
根据这个我们就能得到1-8的lowbit分别为
1 2 1 4 1 2 1 8
1.2管理区间
了解玩lowbit函数,那么回到我们根本的问题上,怎么将前缀和优化,达到相同的效果,但是更新的时候又不用O(N)的复杂度呢?
树状数组的做法是:
第i个位置管理
[i - lowbit(i) + 1, i]
区间,其实这里就可以换成区间或者其他我们想要的结果
只要顺着数字往下画竖线,碰到的第一根横线所覆盖的范围就是它能管理的范围。
每个数的前缀和改成了管理区间了,那么改成这个管理区间怎么优化更新呢?
比如我们需要将x的位置值+k,在前缀和里面我们是不是需要将所有排在x后面的数都+k,在树状数组就只需要找到能够管理到x的位置,+k就好了,这样一来,需要更新的复杂度就变成O(N)了
并且找到管理x位置的下标,只需要y = x + lowbit(x)
,然后迭代更新管理y位置的下标, z = y + lowbit(y)
直到超过数组边界就好了 。
怎么理解这个管理区间i - lowbit(i) + 1, i]
和查找管理i
的下标i + lowbit(i)
呢
我们用八位举例,对于00101000
我们先看i - lowbit(i) + 1 = 00100001,这就意味着,0010 0001-0010 1000都归 0010 1000管
对于0010 0100他的直系管理者 i + lowbit(i) = 0010 1000
但是对于0010 0010,他的直系管理者其实是i + lowbit(i) = 0010 0100,间接管理者才是0010 1000
综上,将前缀和改成管理区间后,更新x只要更新x的管理者就可以了,而管理者的数量是logX数量级的,X为数组长度len - x下标
2.求区间和
聊完更新,我们不能忘乐树状数组的本心,其实是为了求区间结果,管理区间只是为了让树状数组可以持续的接受更新,然后求更新后的区间。
我们前面知道,对于区间和而言,数组的每个元素的数值其实是他管理区间的所有元素和,
所以对于任意的[1,R]区间和,我们其实可以通过迭代求出结果
首先我们求出R和他的管理区间的和也就是位置R的数值,然后对于R - lowbit®也就是第一个R没有管理到的位置,求他的管理区间和,然后迭代求第一个没管理到的位置区间和,由此直到1,得到最终结果。
这样区间和的复杂度也就是O(logN)
所以求[L,R]的区间和,就是[1,L-1]和[1,R]区间和的差值
感谢您能看到末尾!