本文将介绍几求解数组前缀和和连续子数组和的三种方法,分别是遍历法、辅助数组法、树状数组法。
先来定义我们的问题,假设数组为A=[a[0],a[1],a[2],...,a[n]],我们想要求解A[from:to]的和,即求解a[from]+a[from+1]+....+a[to]。
遍历法很简单,即从from的位置循环到to的位置,将元素加起来,代码如下:
package RangeSum;
public class Traversal {
public static int traversal(int[] arr,int start,int end){
int res = 0;
if(start < 0 || start >arr.length || end > arr.length){
System.out.println("index error");
return -999;
}
for(int i=start;i<=end;i++){
res += arr[i];
}
return res;
}
public static void main(String[] args){
int[] arr = {5,3,2,8,7,10,13,6};
//2 + 8 + 7 + 10 + 13 = 40
System.out.println(traversal(arr,2,6));
}
}
遍历法很简单,若区间长度为m,那么求解的时间复杂度为O(m),空间复杂度为O(1)。
遍历法求解简单,单次求解的情况下非常适用。但是当我们需要频繁求解连续子数组和时,就不是那么适用了,这时候,我们便有了辅助数组法。
辅助数组法比较适用于频繁求解连续子数组和的情况,此时,我们增加辅助数组s,s[m]代表0到m的元素和,代码如下:
package RangeSum;
public class AuxiliaryArr {
public static int[] auxiliary(int[] arr){
if(arr == null || arr.length <= 0)
return null;
int[] aux = new int[arr.length];
aux[0] = arr[0];
for(int i=1;iarr.length || end > arr.length){
System.out.println("index error");
return -999;
}
else if(start == 0){
return arr[end];
}
else{
return arr[end] - arr[start-1];
}
}
public static void main(String[] args){
int[] arr = {5,3,2,8,7,10,13,6};
//2 + 8 + 7 + 10 + 13 = 40
int[] auxiliaryArr = auxiliary(arr);
System.out.println(sumRange(auxiliaryArr,2,6));
}
}
辅助数组法的建数组时间复杂度时O(n),空间复杂度为O(n),当频繁求解子数组和时,求和复杂度为O(1)。
但是,当我们回频繁修改数组a时,辅助数组法也不是那么适用,因为修改数组a,辅助数组s也是要更新的,最坏的情况下,我们更新a[0],那么辅助数组的每一个元素都需要修改。如果实时对数组a进行M次修改和求和,那么最坏情况下的时间复杂度时O(M * n)。
这种情况下,有没有更好的解决方法呢!本文的重头戏,树状数组法就要出马了,如果实时对数组a进行M次修改和求和,树状数组的时间复杂度可以达到O(M * logn)。我们一起来看一下。
假设我们有数组A={1,2,3,4,5,6,7,8},我们首先构造一颗二叉树,如下图所示:
随后进行变形,其实什么都没做,只是为了后面看的清晰:
随后,我们定义树状数组C:
可以看到,树状数组中根节点和所有的左子节点都被赋予了相应的值,而且:
C[1]=A[1];
C[2]=A[1]+A[2];
C[3]=A[3];
C[4]=A[1]+A[2]+A[3]+A[4];
C[5]=A[5];
C[6]=A[5]+A[6];
C[7]=A[7];
C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];
怎么知道左子节点在树状数组中的索引呢?其实就是从叶子结点开始往上找,比如C[4],从叶子结点的第四个,由于它是右子节点,所以向上找父节点,发现父节点仍然是右子节点,所以再往上找,发现此时父节点为左子节点,停止寻找。
上面树状数组中的元素,分别是原数组中连续子数组求和得到的,那么怎么知道是哪些元素的求和呢?可以看到,C[m]对应的连续子数组的末尾元素一定是A[m],关键是如何找到起始的元素。
我们首先将十进制数字转换为二进制表示,看能不能发现一些规律:
1=(001) C[1]=A[1];
2=(010) C[2]=A[1]+A[2];
3=(011) C[3]=A[3];
4=(100) C[4]=A[1]+A[2]+A[3]+A[4];
5=(101) C[5]=A[5];
6=(110) C[6]=A[5]+A[6];
7=(111) C[7]=A[7];
8=(1000) C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];
结合树来看一下:
发现规律了没:
1、最后一个1出现在末位,即二进制末尾有0个0,C[m]是一个元素的和,即C[m]=A[m]
2、最后一个1出现在倒数第二位,即二进制末尾有1个0,C[m]是两个元素的和,即C[m] = A[m] + A[m-1]
3、最后一个1出现在倒数第三位,即二进制末尾有2个0,C[m]是4个元素的和....
4、最后一个1出现在倒数第k+1位,即二进制末尾有k个0,C[m]是2^k个元素的和。
因此,我们可以得到计算公式,k为m的二进制中从最低位到高位连续零的长度:
C[m]=A[m-2^k+1]+A[to-2^k+2]+......A[m];
好了,回到我们最开始的问题,如何在原数组更新频繁和多次求解的情况下,快速解决连续子数组求和的问题呢?
那么求解子数组和的问题可以转化为如下的递归形式:
sumRange[from:to]
= A[from] + A[from + 1] +..+A[to]
= sum[from:to - 2^k] + C[to]
C[to] = A[to-2^k+1]+A[to-2^k+2]+......A[to];
好了,来看下代码实现吧。
我们首先定义一个lowbit函数,lowbit(m)=2^k:
public static int lowbit(int t){
return t & (-t);
}
随后我们定义两个数组,分别是原数组和树状数组,这里,我们在初始化的时候,在传入的数组前面增加了一个元素,这样,就跟我们上面的讲解保持一致,即数组的元素下标从1开始:
int[] arr = null;
int[] bitArr = null;
int n = 0;
public BIT(int[] arr){
this.arr = new int[arr.length + 1];
for(int i=0;i
在初始化树状数组时,我们使用了更新树状数组元素这个函数,更新的时间复杂度是O(logn),因为只需要从叶子结点开始,不断向上直到根节点即可:
public void update(int index){
for(int i=index;i <=n ;i += lowbit(i)){
this.bitArr[i] += this.arr[index];
}
}
随后便是分段求和的函数:我们想要求原数组from到to位置的元素和,那么使用树状数组中的对应位置便是from+1到to+1位置,我们这里使用两段和相减得到最终结果,分别是[1,to+1],[1,from],这样,二者相减便是最终结果。
public int sum(int from1,int to1){
int ans1 = 0;
int ans2 = 0;
int from = from1 + 1;
int to = to1 + 1;
for(int i=to;i>0;i-= lowbit(i)){
ans1 += this.bitArr[i];
}
if(from1 > 0){
for(int i=from-1;i>0;i-= lowbit(i)){
ans2 += this.bitArr[i];
}
}
return ans1-ans2;
}
完整的代码如下:
package RangeSum;
public class BIT {
int[] arr = null;
int[] bitArr = null;
int n = 0;
public BIT(int[] arr){
this.arr = new int[arr.length + 1];
for(int i=0;i0;i-= lowbit(i)){
ans1 += this.bitArr[i];
}
if(from1 > 0){
for(int i=from-1;i>0;i-= lowbit(i)){
ans2 += this.bitArr[i];
}
}
return ans1-ans2;
}
public static int lowbit(int t){
return t & (-t);
}
public static void main(String[] args){
int[] arr = {1,2,3,4,5,6,7,8};
//System.out.println(traversal(arr,2,6));
BIT bit = new BIT(arr);
System.out.println(bit.sum(0,6));
}
}