首先我们搞明白树状数组是用来干嘛的,现在有一个这样的问题:有一个数组a
,下标从0
到n-1
,现在给你w
次修改,q
次查询,修改的话是修改数组中某一个元素的值;查询的话是查询数组中任意一个区间 [left,right] 的和。
这个问题很常见
- 首先分析下朴素做法的时间复杂度,修改是O (1) 的时间复杂度,而查询的话是O(n)的复杂度,总体时间复杂度为 O(qn);
- 可能你会想到前缀和来优化这个查询,我们也来分析下,查询的话是O(1)的复杂度,而修改的时候修改一个点,那么在之后的所有前缀和都要更新,所以修改的时间复杂度是O(n),总体时间复杂度还是O(qn)。
接下来我们来看一下树状数组的做法。
这里我们先不管树状数组这种数据结构到底是什么,先来了解下lowbit(x)
这个函数,我们也先不要问这个函数到底在树状数组中有什么用;
顾名思义,lowbit
这个函数的功能就是求某一个数的二进制表示中最低的一位1
,举个例子,x = 6
,它的二进制为110
,那么lowbit(x)
就返回2
,因为最后一位1
表示2
。
在这里,我们提供两种lowbit的实现方法
int lowbit(int x)
{
return x-(x&(x-1));
}
int lowbit(int x)
{
return x&-x;
}
树状数组的思想
在树状数组的问题模型中已经有所提及了,就是那两种不同做法的一个综合;
先定义一些东西:arr
是原数组,c
是新的一个数组,这个数组代表后缀和;
二进制的视角:一个数n
,假设n = 6
,它的二进制为110
,那么我们要求前6
项的和是不是可以这样求:
注意括号中的元素个数,是不是4(100)
个加2(10)
个,和110 = 100 + 10
是不是很像,不知你们发现了吗,10
就是lowbit(110)
的结果,100
是lowbit(100)
的结果。求和的时候我们总是把拆分成这样的几段区间和来计算,而如何去确定这些区间的起点和长度呢?就是根据
n
的二进制来的(不懂的可以再看下上面举的例子),二进制怎么拆的,你就怎么拆分,而拆分二进制就要用到上面说的lowbit
函数了。这里也可以顺理成章得给出c
数组的表示了。
这里也可以顺理成章得给出c
数组的表示了,c[i]
表示从第i
个元素向前数lowbit(i)
个元素,这一段的和,这就是上面说的区间和,只不过这个区间是靠右端点的;你可能又会想,不是说区间是靠右端点的吗,是后缀和啊,那中间的这些区间怎么定义?其实递归定义就好了,比如说
因此我们可以得出
lowbit(1) = 0001 = 1 即 c[1]代表前一个元素的和
lowbit(2) = 0010 = 2 即 c[2]代表前两个元素的和
lowbit(3) = 0011 = 1 即 c[3]代表前一个元素的和
lowbit(4) = 0100 = 4 即 c[4]代表前四个元素的和
lowbit(5) = 0101 = 1 即 c[5]代表前一个元素的和
lowbit(6) = 0110 = 2 即 c[6]代表前两个元素的和
lowbit(7) = 0111 = 1 即 c[7]代表前一个元素的和
lowbit(8) = 1000 = 8 即 c[8]代表前八个元素的和
树状数组的实现
设计一种数据结构,需要的操作无非就是”增删改查“,这里只讨论查询和修改操作具体是怎么实现的;
查询
这里说的查询是查询任一区间的和,由于区间和具有可加减性,故转化为求前缀和;
查询前缀和刚刚在树状数组的思想中已经说过了,就是把大区间分成几段长度不等的小区间,然后求和。区间的个数为O(logn),所以查询的时间复杂度为O(logn)。
修改
修改某一位置上的元素的时间复杂度为O(1),但是要更新c
数组,不然查询的时间复杂度就会变高。
更新的时候只要更新修改这个点会影响到的那些后缀和(c
数组),假设现在修改6(110)
这个点,依据树状数组的性质,它影响的直系父层就是c[6(110) + lowbit(6(110))] = c[8(1000)]
,但是它肯定不是只影响直系父层,上面所有包含这一层和的层都要更新,但是我们把这个更新传递给直系父层c[8]
,8
这个点的直系父层是c[16]
,依次类推地更新就行了。
接着我们来看树状数组的实现
给你一个数组
nums
,请你完成两类查询。
- 其中一类查询要求 更新 数组
nums
下标对应的值- 另一类查询要求返回数组
nums
中索引left
和索引right
之间( 包含 )的nums元素的 和 ,其中left <= right
class NumArray {
private:
vector<int> tree;
vector<int>& nums;
int lowBit(int x)
{
return x&-x;
}
void add(int index,int val)//对应修改操作
{
while(index < tree.size())
{
tree[index] += val;
index += lowBit(index);
}
}
int prefixSum(int index)//对应查询操作
{
int sum = 0;
while(index > 0)
{
sum+=tree[index];
index -= lowBit(index);
}
return sum;
}
public:
NumArray(vector<int>& nums):tree(nums.size()+1),nums(nums)
{
for(int i=0;i<nums.size();++i)
{
add(i+1,nums[i]);
}
}
void update(int index, int val) {
add(index+1,val-nums[index]);
/*
这里的 val - nums[index]是什么意思呢?
假设nums[index]原来的值是x,现在我们要将他修改为y
那么我们就可以让x加上(y-x)让它变为y
即 x+(y-x) = y
*/
nums[index] = val;
}
int sumRange(int left, int right) {
return prefixSum(right+1)-prefixSum(left);
}
};