现在的位置: 首页 > 综合 > 正文

树状数组 详解

2017年10月18日 ⁄ 综合 ⁄ 共 3151字 ⁄ 字号 评论关闭

对于普通数组,其修改的时间复杂度位O(1),而求数组中某一段的数值和的时间复杂度为O(n),因此对于n的值过大的情况,普通数组的时间复杂度我们是接受不了的。

在此,我们引入了树状数组的数据结构,它能在O(logn)内对数组的值进行修改和查询某一段数值的和。

树状数组是一个查询和修改复杂度都为log(n)的数据结构,假设数组a[1..n],那么查询a[1]+...+a[n]的时间是log级别的,而且是一个在线的数据结构,支持随时修改某个元素的值,复杂度也为log级别。

 

假设A[]数组为存储原来的值得数组,C[]为树状数组。

我们定义:C[i] = A[i - 2^k + 1] + ..... + A[i]  其中k为i用二进制表示时的末尾0的个数。例如:i= 10100,则k = 2,i = 11000,则k = 3;为了方便我直接写的是二进制的i,请读者注意。

此时我们可以知道,C[i] 它里面包含了2^k个A[]元素,这2^k个元素是从C[i]往后一直递减的2^k个元素,即i 一直减小的。

其中我们有一种快速的求解2^k的值得方法:

int lowbit(int x){
return x&(x^(x–1));
}

利用机器的补码原理也可以写成这样:

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

下面我们还要求的就是如何快速的修改某一个元素的值以及求出某一段元素值的和;

 

先来看它是怎么快速修改某一个元素的值的:

我们举个例子(为了方便i的值直接写成二进制了):i = 11000,此时k = 3;

这2^k = 3 个数即为:A[11000],A[10111],A[10110],A[10101],A[10100],A[10011],A[10010],A[10001]

C[11000] = A[11000]+A[10111]+A[10110]+A[10101]+A[10100]+A[10011]+A[10010]+A[10001];

这里我们会发现:

A[10100]  + A[10011] +  A[10010] + A[10001] = C[10100]

A[10010] + A[10001] = C[10010]

A[10011] = C[10011]

所以

C[10100] = C[10010] + C[10011] + A[10100]

 

A[10110] + A[10101] = C[10110]

A[10101] = C[10101]

所以

C[10110] = C[10101] + A[10110]

 

A[10111] = C[10111]

 

至此我们可以得出:

C[11000] = C[10100] + C[10110] + C[10111] + A[11000]

 

到这里我们可以得出:

k的值就表示子树的个数,子树即为树状数组的元素。

如图例:

这个个数就是可以从左往右一直加1,怎么加呢?只要保持这k个位左边都是1,右边都是0,且左边至少有一个1,右边可以没有0,而k那个位,原来是1,在加的过程中,就要变成0.如上例:k个位就是11000的末尾3个0,000;就这3个位而言,左边至少一个1,右边可以没有0,就有这三种:100,110,111.再就是把原来第k个位的1变成0,这样就可以得出它的三个子树为:10100,10110,10111.

这样得出的个数就是子树的个数,也等于k的值。

也许有读者会有疑问了,为什么就一定是这样呢?

那是因为,根据树状数组的定义,我们再根据二进制的加法进位的原则,可以得出任何数的树状数组值都可以用上述分解的子树的树状数组元素的值累加得到。(可以根据之前的例子进行理解,务必理解清楚)

有了上述的理解基础了,那么我们就可以知道如何快速的根据第i个节点求出它的父节点了,求法如下:

i的父节点为p,则p = i + lowbit(i);

根据前面的知识,这一点毋庸置疑。

 

现在我们再来理解一个知识:

k 表示的 为i 这个节点表示的树的深度。

为什么呢?

我们知道k为i的末尾0的个数,而小于i的节点的值肯定要小于i,那么它们的k绝对要小于i的k,而最长的就是k,因为它的二进制表示的数只能允许它右移k位,右移k位之后它就是叶子节点了,就只表示一个单一的A[]数组的值了,同时也是C[]树状数组的值。

 

有了这点知识为基础,那么我们就可以知道,我们要修改某个元素的值,就会修改C[]的值,以及它的所有祖先节点的值。

而我们已经知道,它的父节点的节点编号就是i + lowbit[i],一步就可以返回过去,而这个树的深度只有logn,所以我们往上一步一步的修改它的祖先节点就行了,且最多只要logn步,因此时间复杂度是O(logn)。实现函数代码如下:

void plus(int pos , int num) 
{ 
    while(pos <= n) 
    { 
          in[pos] += num; 
          pos += Lowbit(pos); 
    } 
} 

快速修改已经实现了,那么接下来就要快速求出某一段元素的值的和了:

我们先来快速求出前n项元素的和,只要能快速求出前n项元素的和,那么快速求某一段元素的和就只要做一次减法就OK了。

那么前n项元素的和怎么快速的求出来呢?

我们来看看i = 11000(2进制),这个数,如何快速求出前i个元素的和。

我们知道:11000 = 2^3 + 2^4

也就是它总共有2^3+2^4 个元素,我们回想一下前面的知识,C[11000]包括了几个元素,2^3个?对!,就是2^k,k=lowbit(11000) 个,而这2^3个就是11000一直递减的2^3个,而还有2^4个呢?聪明的读者会发现那是不是就是C[10000]包括的元素的个数呢?就是2^k,k=lowbit(10000).而这2^4个刚好是接着11000的2^3个之后的2^4个。

所以S[11000] = C[11000] + C[10000],S[i]为前i项的和。

现在我们来想想为什么就是这么刚好呢?

我们根据二进制的组成,可以得出,任何一个数i都可以表示成2的幂次方的和的一个组成形式,而树状数组C[i]表示的是2^k,k=lowbit(i)个元素相加的和,这些元素是从第i个元素往下数i一直递减而得到的。那么我们是否可以用C[]的一些元素相加来求得S[]呢?

答案是可以的,刚好可以,k的值表示的是从这个数开始往下一直数掉2^k个,这2^k个数的值的和存在C[i]里,而数掉的这2^k个只会影响到第k位上的那个1的变动,变成了0。当数掉了2^k个数之后,

此时如果再往下数,会产生什么样的影响呢?会影响到k+1位的1的变动,变成0.我们先想一下,我们数掉2^k个数之后,得到的数不就是一个末尾是k+1个0个数了么!

因为2^k个数数完了,且在只有k个0的情况下,数完2^k个数之后末尾又会全是0,而第k位的1也会变成了0,所以此时这个数的末尾0的个数就是k+1,它可以往下数2^(k+1)个数,而这2^(k+1)个数的值的和刚好存在C[i - lowbit(i)]里。到这里我想聪明的读者肯定已经知道如何快速的求出前n项元素的和了。

我们只要一直往下数,数到左边没有1可以变动的时候,即我们从i数到了0,那么这i个数你就数完了,答案也就出来了。

在这个过程中,我们可以知道,我们每次要变动某个1的时候,我们都知道从它开始往下数的2^k,k=lowbit(i)个数的值的和,这个个总和存在了C[i]里,而每一个数n它最多能变动的1的个数只有logn个,所以我们在计算前n个元素的和的时候最多只要计算logn次的加法,因此求前n个元素的和的时间复杂度即为O(logn)。

下面给出函数实现代码:

int Sum(int end) 
{ 
    int sum = 0; 
    while(end > 0) 
    { 
        sum += in[end]; 
        end -= Lowbit(end); 
    } 
    return sum; 
}

至此,我们实现了在O(logn)的时间内,修改数组的元素和求得某一段元素的和了。

 

抱歉!评论已关闭.