最近在学习位运算,正好把树状数组总结下,也算是能正式给data structure
建个分类。算法
那么,树状数组到底有什么用呢?诚然,同样没什么卵用的东西咱们学它干吗。数组
下面举个树状数组的经典应用:区间求和。数据结构
假设咱们有以下数组(数组元素从 index=1
开始):函数
var a = [X, 1, 2, 3, 4, 5, 6, 7, 8, 9];
咱们设定两种操做,modify(index, x)
表示将 a[index]
元素加上x, query(n, m)
表示求解 a[n] ~ a[m]
之间元素的和。若是不了解树状数组(固然假设更不了解线段树等其余数据结构),你可能会很容易地写下以下代码:学习
var a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; function query(n, m) { var sum = 0; for (var i = n; i <= m; i++) sum += a[i]; return sum; } function modify(index, x) { a[index] += x; }
Ok,复杂度为O(1)的删改和复杂度为O(n)的查询。若是数据量很大,这样反复的查询是至关耗时的。咱们退一步想,若是只有 query(n, m)
这个操做,很容易想到用sum数组预处理前n项的和,而后用 sum[m] - sum[n-1]
得到答案。可是若是要修改 a[index]
的值,由于该项影响全部index以后的sum数组元素,因此若是这样作复杂度变为O(1)的查询和O(n)的删改,并无什么卵用。code
可是这个思路是美好的,咱们能够用一个sum数组保存一段特定的区间段的值。假设咱们有 a[1] ~ a[9]
9个元素,咱们根据一个特定的规则:blog
sum[1] = a[1]; sum[2] = a[1] + a[2]; sum[3] = a[3]; sum[4] = a[1] + a[2] + a[3] + a[4]; sum[5] = a[5]; sum[6] = a[5] + a[6]; sum[7] = a[7]; sum[8] = a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7] + a[8]; sum[9] = a[9];
若是要求 a[1] ~ a[9]
的和,即为 sum[9] + sum[8]
,若是要求 a[1] ~ a[7]
的和,即为 sum[7] + sum[6] + sum[4]
,若是要改变 a[1]
的值,改变sum数组中和 a[1]
有关的项便可(即 sum[1]
sum[2]
sum[4]
sum[8]
)。 这就是树状数组!实现了O(logn)的查询和删改。可是如何将a数组和sum数组联系起来?it
来观察这个图:
io
令这棵树的结点编号为C1,C2...Cn。令每一个结点的值为这棵树的值的总和,那么容易发现(如上所说):function
C1 = A1 C2 = A1 + A2 C3 = A3 C4 = A1 + A2 + A3 + A4 C5 = A5 C6 = A5 + A6 C7 = A7 C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
这里有一个有趣的性质:设节点编号为x,那么这个节点管辖的区间为 2^k
(其中k为x二进制末尾0的个数)个元素。由于这个区间最后一个元素必然为Ax,因此很明显:Cn = A(n – 2^k + 1) + ... + An,算这个2^k有一个快捷的办法,定义一个函数以下便可(求解2^k即求二进制码右边第一位1的值):
int lowbit(int x) { return x & (-x); }
当想要查询一个SUM(n)(求a[1]~a[n]的和),能够依据以下算法便可:
能够看出,这个算法就是将这一个个区间的和所有加起来。
那么修改呢,修改一个节点,必须修改其全部祖先,最坏状况下为修改第一个元素,最多有log(n)的祖先。因此修改算法以下(给某个结点i加上x):
关于这部分的代码,将在下文树状数组的具体三大应用中给出。
关于树状数组,有一点须要注意,为了方便,树状数组的a数组基本都是从 index=1
开始的。
下文中楼主会分析下树状数组的三大应用场景:改点求段,改段求点,改段求段。