算法篇:树状数组


文章最后修改时间:2020-08-30 19:15

树状数组(二叉索引树)

  树状数组又称二叉索引树(Binary Indexed Tree),主要用于快速

一、前缀和数组

1. 数列与其前缀和数列

  对于数列 a = { a 1 , a 2 , a 3 , . . . , a n } a =\{a_1, a_2, a_3, ..., a_n\} a={a1,a2,a3,...,an},简写为 { a n } \{a_n\} {an},其第 n n n 项为 a n a_n an
  数列的前缀是指从数列第一项开始的连续子数列。
  例如:对于数列 { a n } \{a_n\} {an},其前缀有 { a 1 } \{a_1\} {a1} { a 1 , a 2 } \{a_1,a_2\} {a1a2} { a 1 , a 2 , a 3 } \{a_1,a_2, a_3\} {a1a2a3}, { a 1 , a 2 , a 3 , . . . , a n − 1 } \{a_1,a_2, a_3, ...,a_{n-1}\} {a1a2a3,...,an1}以及 { a n } \{a_n\} {an}本身。
  前缀和就是指数列 { a n } \{a_n\} {an}的某个前缀序列中所有元素之和,即平常说的 数列前 n n n项之和
  这里定义数列 { a n } \{a_n\} {an} 的前 n n n 项之和,
S n = ∑ i = 1 n a i S_n=\sum_{i=1}^n{a_i} Sn=i=1nai
  即 S n = a 1 + a 2 + a 3 + . . . + a n S_n = a_1 + a_2 + a_3 + ... +a_n Sn=a1+a2+a3+...+an

  对于数列 { a n } \{a_n\} {an},由 S n S_n Sn 组成的数列 { S n } \{S_n\} {Sn} 称为 { a n } \{a_n\} {an}的前缀和数列。并且可以得到以下公式:
S n = S n − 1 + a n S_n = S_{n-1} +a_n Sn=Sn1+an S n − S m = ∑ i = m + 1 n a i , ( n > m ) S_n - S_m = \sum_{i=m+1}^{n}{a_i} ,\quad(n> m) SnSm=i=m+1nai,(n>m)

2. 前缀和数组

在编程语言中,数组的起始索引一般从0开始,这源于偏移的概念。

由于数组 a a a 的前缀和数组 S S S, 有以下关系
S [ n ] = ∑ i = 0 n a [ i ] S[n]=\sum_{i=0}^n{a[i]} S[n]=i=0na[i]
前缀和数组中的元素 S [ n ] S[n] S[n],其值为原数组 a a a 中下标从0到 n 的元素之和。

S[0] = a[0]
S[1] = a[0] +a[1]
S[2] = a[0] + a[1] + a[2]

S[n] = a[0] + a[1] + a[2] + … + a[n]

在这里插入图片描述

2.1 前缀和数组的计算

  对于数组 a \boldsymbol{a} a, 利用公式 S n = S n − 1 + a n S_n = S_{n-1} +a_n Sn=Sn1+an,很容易递推得到其前缀和数组计算方法。
{ S [ 0 ]    =    a [ 0 ] , S [ n ]    =    S [ n − 1 ] + a [ n ] \begin{cases} S\left[ 0 \right] \,\,=\,\,a\left[ 0 \right] ,\\ S\left[ n \right] \,\,=\,\,S\left[ n-1 \right] +a\left[ n \right]\\ \end{cases} {S[0]=a[0],S[n]=S[n1]+a[n]

int a[n], s[n];

s[0] = a[0];

for (int i = 1; i < n; i++) {
	s[i] = s[i-1] + a[i];
}

3. 前缀和数组的用途

  得到其前缀和数组后,可以快速得到数组 a a a 的前 n n n 项和,以及由 S [ n ] − S [ m − 1 ] S[n] - S[m-1] S[n]S[m1] 即可求出数组 a a a [ m , n ] [m, n] [m,n] 区间的元素和。
  简而言之,通过前缀和数组,我们可以在 O ( 1 ) O(1) O(1)时间内快速求出数组某一区间内元素之和。

4. 前缀和数组的复杂度分析

4.1 空间复杂度O(n)

  对于长度为 n n n 的数组 a a a,其前缀和数组 S S S 长度也为 n n n,所以空间复杂度为 O ( n ) O(n) O(n)

4.1 时间复杂度
4.1.1 创建

  前缀和数组的创建只需要遍历一遍原数组,进行 n n n 次求和操作,复杂度为 O ( n ) O(n) O(n).

4.1.2 查询

  前缀和数组 查询某个前缀和可直接读取 前缀和数组 中对应索引的元素 S [ i n d e x ] S[index] S[index],无需额外计算,时间复杂度为 O ( 1 ) O(1) O(1).

4.1.3 更新

  如果数组中某个元素 a [ i ] a[i] a[i] 的值发生了变化,为了维护前缀和数组,那就需要对前缀和数组S中 S [ i ] S[i] S[i] S [ i ] S[i] S[i]之后的所有前缀和进行更新,更新前缀和数为 n − i + 1 n-i+1 ni+1.
  随机情况下,平均需要更新 n + 1 2 \frac{n+1}{2} 2n+1次,复杂度为 O ( n ) O(n) O(n)

5. 前缀和数组适用性

  从前面可以得到,前缀和数组查询复杂度为 O ( n ) O(n) O(n),但是如果有一个元素,维护的代价为 O ( n ) O(n) O(n)
  在元素频繁更新的情况下,前缀和数组就不太适用了。
  前缀和数组适合创建后元素不改变或者极少改变,只需要大量查询的情况。





二、树状数组

在这里插入图片描述

1. 产生原因

  上面已经说到,前缀和数组在元素频繁更新情况下维护开销是 O ( n ) O(n) O(n) 的,这对于元素更新不利,希望能有一种数据结构,可以降低这种复杂度。
  为了解决前缀和数组在元素频繁更新情况下维护开销较大的情况,产生了树状数组。

2. 作用

  将元素更新时的维护开销由 O ( n ) O(n) O(n) 降低至 O ( log ⁡ n ) O(\log n) O(logn),但与此同时也付出了相应的代价,增加了查询的开销,查询复杂度由 O ( 1 ) O(1) O(1) 变成了 O ( log ⁡ n ) O(\log n) O(logn)

3. 优化思路

3.1 前缀和与原数组的包含关系

  对于前缀和数组,有如下关系:(这里下标从1开始,对于前缀和数组来说没什么影响)
S [ n ] = ∑ i = 1 n a [ i ] S[n]=\sum_{i=1}^n{a[i]} S[n]=i=1na[i]
  前缀和数组中每个元素 S [ x ] S[x] S[x]直接存储从数组 a a a中第一个开始至第 x x x个的所有元素之和。元素 a [ i ] a[i] a[i]在元素 S [ j ] S[j] S[j]求和区间内的,暂且称之为 S [ j ] S[j] S[j] 的求和区间包含 a [ i ] a[i] a[i]

  如下图所示,下图中,前缀和数组 S S S元素的长度不同,这显示了前缀和的求和区间。
  在前缀和数组 S S S的元素下方的数组 a a a元素在其求和区间内。
  并且可以得到,如果 a [ i ] a[i] a[i] 的数值更新,那么求和区间包含 a [ i ] a[i] a[i] 的所有前缀和数值都要更新
在这里插入图片描述

3.2 分区间求和

  前面说到,前缀和数组里每个元素的求和区间,直接将前缀和需要求和的元素全部包含,这就会导致元素被包含的次数过多。(直接来看就是层数多,层数是随着数组长度线性增长的)使得在元素更新时,所有求和区间包含它的前缀和都需要重新计算。
  如果我们能把包含元素的求和区间数降下来,那么就能减少元素更新时的维护开销。
  于是使用了分治的思想,把本来的前缀和区间分割成多个小区间,存储这些小区间的和值。实际求前缀和时,再找出这些区间的和值进行求和,得到正确的前缀和。

  那么如何分区间才能使每个前缀和区间都能由分好的小区间组合而无遗漏呢?
  下图给出了其中一个答案,如下图,这些区间的划分排布是不是有种熟悉感?这就是根据二进制来划分,也是树状数组的区间划分方式。

在这里插入图片描述

3.2.1 C [ i ] C[i] C[i]的求和区间

  如上图的浅紫色区域所示,每一个层对应一个二进制位。越高层对应越高的二进制位。
   C [ i ] C[i] C[i]在第几层,取决于 i i i 二进制形式最低位的1,即 l o w b i t ( i ) lowbit(i) lowbit(i)
  例如, C [ 12 ] C[12] C[12] ,数值12的二进制形式为 110 0 b 1100_b 1100b , 最低位的1为 010 0 b 0100_b 0100b,所以在 010 0 b 0100_b 0100b这层 (从下往上数第三层)
  而 C [ i ] C[i] C[i] 所在的层也直接对应了 C [ i ] C[i] C[i] 求和区间的长度。在 010 0 b 0100_b 0100b这层的 C [ i ] C[i] C[i],求和区间长度就为4。所以 C [ i ] C[i] C[i] 求和区间的长度也等于 l o w b i t ( i ) lowbit(i) lowbit(i)
   C [ i ] C[i] C[i]的求和区间,最后包含的一个元素就是 a [ i ] a[i] a[i],而区间长度为 l o w b i t ( i ) lowbit(i) lowbit(i),所以包含的元素是从 a [ i − l o w b i t ( i ) + 1 ] a[i - lowbit(i) + 1] a[ilowbit(i)+1]开始,于是得到 C [ i ] C[i] C[i] 与 数组 a a a 中元素的关系:
C [ i ] = ∑ i − l o w b i t ( i ) + 1 i a [ i ] C[i] = \sum_{i-lowbit(i)+1}^{i}{a[i]} C[i]=ilowbit(i)+1ia[i]

3.2.2

复杂度

  树状数组是用来维护数列前缀和的。特点是数组元素更新后,可以用   O ( log ⁡ n ) \ O(\log n)  O(logn)的时间复杂度就可以完成前缀和的维护,当然,在查询时也是   O ( log ⁡ n ) \ O(\log n)  O(logn)的复杂度。

  • 空间复杂度   O ( n ) \ O(n)  O(n)
  • 查询前缀和   S ( n ) \ S(n)  S(n)操作:   O ( log ⁡ n ) \ O(\log n)  O(logn)
  • 数组元素更新后,完成前缀和更新:   O ( log ⁡ n ) \ O(\log n)  O(logn)

  如果对于一个数组A,如果用S来表示保存前缀和,S[i] 表示 A[0] 到A [n]的数列和,查询前缀和时很快,由S[i]就可以直接得出,但是如果数组A有元素增减,前缀和数组S要 平均要付出   O ( n ) \ O(n)  O(n)的代价来更新。
  而树状数组可以做到以   O ( log ⁡ n ) \ O(\log n)  O(logn)的时间复杂度求前缀和,元素变化时也是以   O ( log ⁡ n ) \ O(\log n)  O(logn)的时间复杂度完成更新。

  树状数组如下图,C是数组,按照下标关系,对应成如下的树, 如果数组长度不为2的次幂,那么将会构成森林。可以看到,每一棵树,叶结点即数组 a a a 的元素,每棵树的根节点一定是2的次幂的数。

在这里插入图片描述

  对于一棵树结点数为 2 N 2N 2N 的树,有 N N N C [ x ] C[x] C[x] 结点,有 N N N A [ x ] A[x] A[x] 结点。 A [ x ] A[x] A[x] C [ x ] C[x] C[x] 的子结点,连线为直边, 所以直边一共有 N N N条,斜边有 N − 1 N-1 N1条。
  如果一个数组的长度N不为2的次幂,那么对应的树状数组将会有多棵树,树的数目等于N写成二进制形式的1的个数,7 = (111)b,有三棵,8 = (1000)b,有一棵。

在这里插入图片描述

树状数组元素与原数组元素的包含关系

  
  如上图所示, C C C数组元素垂直对应的对于数组A,建立相同长度的树状数组C。有如下关系:
C [ x ] = ∑ i = x − l o w b i t ( x ) + 1 x a i C\left[ x \right] =\sum_{i=x-lowbit\left( x \right) +1}^x{a_i} C[x]=i=xlowbit(x)+1xai

  即 C [ x ] C[x] C[x]的值就等于数组 a a a中下标在   [ x − l o w b i t ( x ) + 1 , x ] \ [x-lowbit(x) + 1, x]  [xlowbit(x)+1,x]之间的元素之和。
  如 C [ 6 ] C[6] C[6], 由 l o w b i t ( 6 ) = l o w b i t ( 11 0 b ) = 1 0 b = 2 lowbit(6) = lowbit(110_b) = 10_b = 2 lowbit(6)=lowbit(110b)=10b=2,所以 C [ 6 ] = ∑ i = 5 6 a i = a [ 5 ] + a [ 6 ] C[6] = \sum_{i=5}^6{a_i} = a[5] + a[6] C[6]=i=56ai=a[5]+a[6]

  树状数组逻辑上的下标是从1开始的,而不是从0开始的。树状数组结点之间的关系是由下标值决定的,组成树的元素不包括下标0。

性质

性质:

  • 树状数组每个节点C[x] 的值等于数组   A [ x − l o w b i t ( x ) + 1 ] \ A[x-lowbit(x)+1]  A[xlowbit(x)+1]   A [ x ] \ A[x]  A[x]的和。
  • C[x]的值 也等于C[x]所有子结点的和。C[x]的子节点数等于lowbit(x)的二进制位数,如C[8], lowbit(8) = (1000)b,有四位,有四个子结点
    C[1] = A1
    C[2] = C[1] + A2 = A1 + A2
    C[3] = A3
    C[4] = C[2] +C[3] + A[4] = A1 + A2 + A3 + A4
    C[5] = A5
    C[6] = C[5] +A[6] = A5 + A6
    C[7] = A7
    C[8] = C[4] +C[6] +C[7] + A[8] = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8

  每个 C [ x ] C[x] C[x],子结点都包括 A [ x ] A[x] A[x]。而在C中的子结点,是由x - 小于lowbit(x)的位得到。 对于C[8], 它的子结点除了A[8], 还有C[4], C[6], C[7]。 对于C[8],   l o w b i t ( 8 ) = ( 1000 ) b \ lowbit(8) = (1000)_b  lowbit(8)=(1000)b, 小于   l o w b i t ( 8 ) \ lowbit(8)  lowbit(8)的位有   ( 100 ) b = 4 , ( 10 ) b = 2 , ( 1 ) b = 1 \ (100)_b = 4, (10)_b = 2, (1)_b = 1  (100)b=4,(10)b=2,(1)b=1
  所以C[8]的在C中的子结点分别是   C [ 8 − 4 ] = C [ 4 ] , C [ 8 − 2 ] = C [ 6 ] , C [ 8 − 1 ] = C [ 7 ] \ C[8 - 4] = C[4], C[8-2] = C[6], C[8-1] = C[7]  C[84]=C[4],C[82]=C[6],C[81]=C[7]
  查询C[x]除了A[x]以外的子结点

void subNode(int x)
{
	int lowbit = x & -x;
	int sub;
	while (lowbit >>= 1) {
		sub = x - lowbit;	//子结点编号
		printf("%d", &sub);
	}
}
  • 对于内部结点C[x], 其父节点是 C[x + lowbit(x)]
int lowbit = x & -x;
fatherNode = x + lowbit; 

支持的操作

  树状数组支持查询前缀和单点增加两个基本操作。对于一个已经初始化的树状数组。

  • 由树状数组C查询前缀和S[n]的方法
    想要查询前缀和S[n], 因为树状数组C[x]中保存的是数组A某一段的和。所以求前缀和时,将这几段加起来即可。如A[1] 到A[7]的和,分成了A[1 ~ 4], A[5 ~ 6], A[7]三段, 每一段的和保存在C[4], C[6], C[7]中,所以只要把这三个加起来就等于前缀和S[7]。分段是由n的二进制形式决定,有多少个1就分多少段。   7 = ( 111 ) b = 2 2 + 2 1 + 2 0 = 4 + 2 + 1 \ 7 = (111)_b = 2^2 + 2^1 + 2^0=4+2+1  7=(111)b=22+21+20=4+2+1, 所以分成了   C [ 4 + 2 + 1 ] = C [ 7 ] , C [ 4 + 2 ] = C [ 6 ] , C [ 4 ] \ C[4+2+1] = C[7], C[4 +2] = C[6], C[4]  C[4+2+1]=C[7],C[4+2]=C[6],C[4]这三部分的和, 即不断得进行 x - lowbit(x)操作,到0为止,即可得出所有的和
      由上面的图可以看出,树状数组构成的树是不包括C[0]和A[0]的,我们可以赋值C[0],查询前缀和时额外加上C[0]即可。由此可以我们可以计算前缀和S(x)
int askPrefixSum(int x)
{
	if (x < 0)
		return 0;
	int sum = c[0];
	while (x) {
		sum += c[x];
		x &= (x-1);		//或者 x -= x & -x;
	}
	return sum;
}

  如果要查询A中[l, r]范围内的元素和, 只需计算 askSum (r ) - askSum(l -1)

  • 由前缀和可以求出数组A的元素, A[n] = S[n] - S[n-1]
int getElement(int x) {
	return askPrefixSum(x) - askPrefixSum(x - 1);
}
  • 单点增加, 就是当A中某个元素变化时,更新树状数组的操作。
    当A[x]增量为 y,只要在数组C中不断更新x往上的父结点,都加上y即可。N为数组A的长度。由于树是不包括C[0]和A[0]的,这个需要额外操作。
\\A[x] 的增量为 y后,树状数组更新
void add(int x, int y)
{
	if (x == 0)
		c[0] += y;
	else {
		while (x < N) {
			c[x] += y;
			x += x & -x;
		}
	}
}

初始化

通过树状数组查询前缀和需要对C[n]进行正确初始化。

1. 利用单点操作初始化

  利用单点操作,数组C先初始化为0,然后遍历数组A,对每个元素进行单点操作,即可完成初始化, 单点操作时间复杂度为   O ( log ⁡ n ) \ O(\log n)  O(logn), 所以复杂度为   O ( n log ⁡ n ) \ O(n\log n)  O(nlogn)

void init_add(int a[],int length) {
	for (int i = 0; i < length; i++) {
		add(i, a[i]);
	}
}

2. 利用前缀和数组进行初始化

前面我们知道C[x] 的值保存着 A[x - lowbit(x) +1]A[x] 的和, 即等于前缀和之差, C[x] = S[x] - S[x - lowbit(x)]
x- lowbit(x)x - x & -x或者 x & (x-1)
c[0]不适合上面那个算式,需要单独计算,C[0] = A[0]

  • 时间复杂度   O ( n ) \ O(n)  O(n), 空间复杂度   O ( n ) \ O(n)  O(n)
void init_presum(int a[], int length) {
	int* preSum = new int[length];
	c[0] = preSum[0] = a[0];			//c[0]不适合下面那个算式
	for (int i = 1; i < length; i++) {
		preSum[i] = preSum[i - 1] + a[i];
		c[i] = preSum[i] - preSum[i & (i - 1)];
	}
	delete[] preSum;
}

3. 利用子结点之和初始化

每个结点的值就等于其子结点的值之和,所以求其子结点之和即可。所有的操作相当于将上面树状数组的边遍历一次。由上面的结论,可以得出上面每一棵树的斜边和直边数目和为(2 N - 1), 所以可以得到时间复杂度为   O ( n ) \ O(n)  O(n).

void init_subNode(int a[], int length)
{
	c[0] = a[0];
	for (int i = 1; i < length; i++) {		
		int lowbit = i & -i;
		c[i] = a[i];
		while (lowbit >>= 1) {
			c[i] +=  c[i - lowbit];
		}
	}
}

BinaryIndexedTree类

//file : binaryIndexeTree.h
class BinaryIndexeTree
	{
	private:
		int* c = nullptr;
		int length = 0;

	private:
		void init_subNode(int a[], int length);
		void init_add(int a[], int length);
		void init_presum(int a[], int length);

	public:
		BinaryIndexeTree(int a[], int length);

		~BinaryIndexeTree();

		//查询前缀和S(x)
		int askPrefixSum(int x);

		//查询原数组元素
		int getElement(int x);

		//数组变化A[x] 变化量为 y
		void add(int x, int y);

		void initTree(int a[], int length);	

		//输出树状数组
		void printTree();	

		//输出前缀和数列
		void printPrefixSum();

		//输出原数组元素
		void printElements();
	};
	
//file : BinaryIndexeTree.cpp
#include <cstdio>
#include "binryIndexedTree.h"

BinaryIndexeTree::BinaryIndexeTree(int a[], int length) {
		initTree(a, length);
	}

	BinaryIndexeTree::~BinaryIndexeTree() {
		delete[] c;
	}

	int BinaryIndexeTree::askPrefixSum(int x)
	{
		if (x < 0)
			return 0;
		else if (x >= length)
			return askPrefixSum(length - 1);

		int sum = c[0];
		while (x) {
			sum += c[x];
			x -= x & -x;
		}
		return sum;
	}

	int BinaryIndexeTree::getElement(int x) {
		return askPrefixSum(x) - askPrefixSum(x - 1);
	}

	void BinaryIndexeTree::add(int x, int y)
	{
		if (x == 0)
			c[0] += y;
		else {
			while (x < length) {
				c[x] += y;
				x += x & -x;
			}
		}
	}

	void BinaryIndexeTree::initTree(int a[], int length)
	{
		if (c != nullptr && this->length != length) {
			delete[] c;
			c = new int[length];
		}
		else if (c == nullptr) {
			c = new int[length];
		}

		this->length = length;
		init_subNode(a, length);
	}

	void BinaryIndexeTree::init_subNode(int a[], int length)
	{
		
		c[0] = a[0];
		for (int i = 1; i < length; i++) {
			int lowbit = i & -i;
			c[i] = a[i];
			while (lowbit >>= 1) {
				c[i] += c[i - lowbit];
			}
		}
	}

	
	void BinaryIndexeTree::init_add(int a[], int length) {
		for (int i = 0; i < length; i++) {
			add(i, a[i]);
		}
	}

	void BinaryIndexeTree::init_presum(int a[], int length) {
		int* preSum = new int[length];
		c[0] = preSum[0] = a[0];
		for (int i = 1; i < length; i++) {
			preSum[i] = preSum[i - 1] + a[i];
			c[i] = preSum[i] - preSum[i & (i - 1)];
		}
	}

	void BinaryIndexeTree::printTree()
	{
		printf("tree:[");
		for (int i = 0; i < length; i++) {
			printf("%d ", c[i]);
		}
		printf("]\n");
	}

	void BinaryIndexeTree::printPrefixSum()
	{
		printf("prefixSum:[");
		for (int i = 0; i < length; i++) {
			printf("%d ", askPrefixSum(i));
		}
		printf("]\n");
	}

	void BinaryIndexeTree::printElements() {
		printf("elements:[");
		for (int i = 0; i < length; i++) {
			printf("%d ", getElement(i));
		}
		printf("]\n");
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

依稀_yixy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值