一.概述

树状数组的特点,一句话就是:定点更新,区间求值。例如,数组{a}中的元素可能不断地被修改,它能够高效地获取数组中连续n个数的和。数据规模不大的时候,对于修改某点的值是非常容易的,复杂度是O(1),但是对于求一个区间的和就要扫一遍了,复杂度是O(N),如果实时的对数组进行M次修改或求和,最坏的情况下复杂度是O(M*N).而用树状数组的复杂度却是O(M*lgN),别小看这个lg,当数据规模很大的时候,效率就大大提升了。

二.基本操作

传统数组(共n个元素)的元素修改和连续元素求和的复杂度分别为O(1)和O(n)。树状数组通过将线性结构转换成伪树状结构(线性结构只能逐个扫描元素,而树状结构可以实现跳跃式扫描),使得修改和求和复杂度均为O(lgn),大大提高了整体效率。

A数组就是我们要维护和查询的数组,但是其实我们整个过程中根本用不到A数组!C数组才是我们全程关心和操纵的重心。先由图来看看c数组的规则,其中c8 = c4+c6+c7+a8,c6 = c5+a6……

怎么做到的?解释如下:给定序列(数列)A,我们设一个数组C满足

C[i] = A[i–2^k+ 1] + … + A[i]

其中,k为i最末尾1之后零的个数。比如0d10的二进制是1010,则k=0b10

我们称C为树状数组。
当我们修改A[i]的值时,我们需要从C[i]往根节点一路上溯,调整这条路上的所有C[],这个操作的复杂度在最坏情况下就是树的高度即O(logn)。这个操作看起来有点麻烦,但是对于求数列的前n项和确实有不少好处:如同上面求C8的例子一样,只需要找到n=8以前的所有最大子树,把其根节点的C加起来即可。不难发现,这些子树的数目是n在二进制时1的个数,或者说是把n展开成2的幂方和时的项数,因此,求和操作的复杂度也是O(logn)。

(形式化)求任意区间的和:

求A[i] + A[i+1] + … + A[j],设sum(k) = A[1]+A[2]+…+A[k],则
A[i] + A[i+1] + … + A[j] = sum(j)-sum(i-1)。

知道了树状数组的好处,下面再来说一说C[i]的定义中的A[i-2^k+1]。简单一点解释吧(人家设计的太巧妙,看懂就行了= =):给定i,如何求2^k?

2^k=i&(i^(i-1)) , 或者i&(-i)

求k的java方法:

//求k
public int lowbit(int t){
    return t&(t^(t-1));
}

剩下的就是修改和求和操作了:

//修改数组元素
public void modyValue(int pos,int value){
        while (pos<=arr_len) {//arr_len为数组C长度
            C[pos]+=value;
            pos+=lowbit(pos);
        }
    }
//获得前n项和
public int calSum(int end){
        int sum=0;
        while (end>0) {
            sum+=C[end];
            end-=lowbit(end);
        }
        return sum;
    }

三.问题实例


- 当在网上购物使用关键词搜索商品时,可以选择很多附选条件,比如品牌,价格等,之后会出现符合当前条件的商品及数量,例如搜索词为:羽绒服,附选条件为:价格¥500-¥1000
- 商品数量会因为商家上架、下架商品而改变,因此买家在不同时间段搜索商品时,显示的数量会不一样。
- 接下来包含一个含有多行的文本文件,文件的每行代表上架,下架,查询三种行为的一种,本题默认查询对象是‘衣服’。
请计算所有买家查询结果的总和。

  • 举例: 一个拥有6行的文件。
• up 3 11 (有一个商家上架了3件11rmb的衣服。)
• query 11 25 (有一个买家查询11rmb-25rmb的衣服的数量。这里的查询结果是:3件)
• up 5 25 (有一个商家上架了5件25rmb的衣服。)
• query 11 25 (有一个买家查询11rmb-25rmb的衣服的数量。这里的查询结果是:8件)
• down 3 25 (有一个商家下架了3件25rmb的衣服。)
• query 20 25 (有一个买家查询20rmb-25rmb的衣服的数量。这里的查询结果是:2件)

这题的答案是所有买家查询结果的总和为13(3+8+2=13)。

当数据量为一千行,普通办法解决:

//建了一个长度为100000的Hash数组,
public class GoodsNum {
    public static void main(String[] args) {        
        long startTime=System.currentTimeMillis();
        int[] hash=new int[100000];//存储货物价格和数量的哈希数组
        String pathname="files/myfile.txt";
        try {
            FileReader fr=new FileReader(new File(pathname));
            BufferedReader br=new BufferedReader(fr);//按行读文件
            String str="";
            long sum=0;//结果和的初始化
            while ((str=br.readLine())!=null) {         
                if(str.startsWith("up")){
                    hash[Integer.parseInt(str.split(" ")[2].trim())]+=Integer.parseInt(str.split(" ")[1].trim());
                }
                else if (str.startsWith("down")) {
                    if(hash[Integer.parseInt(str.split(" ")[2].trim())]==0){
                        System.out.println("operation error!");
                        continue;
                    }
                    hash[Integer.parseInt(str.split(" ")[2].trim())]-=Integer.parseInt(str.split(" ")[1].trim());
                    if(hash[Integer.parseInt(str.split(" ")[2].trim())]<0) hash[Integer.parseInt(str.split(" ")[2].trim())]=0;
                }
                else if (str.startsWith("query")){
                    int begin=Integer.parseInt(str.split(" ")[1]);
                    int end=Integer.parseInt(str.split(" ")[2]);

                    for(int i=begin;i<=end;i++){
                        if(hash[i]>0)
                            sum+=hash[i];
                    }
                }
                else {
                    System.out.println("wrong operation!---"+str);
                    break;
                }
            }
            System.out.println(sum);
            System.out.println((System.currentTimeMillis()-startTime)+"ms");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果:

Java树形列表加分页_Java树形列表加分页

好吧,数据量来到了100000行,货物的价格也变大了。(其实并不是很大,那道五十亿行的问题还没想明白咋回事,看看效果)

普通版本运行结果:

Java树形列表加分页_System_02


长了不止一点点…然后是树状数组的版本,先上代码:

public class GoodsNum2 {
    private int arr_len=99999;
    private int []C=new int[arr_len+1];//数组C从1开始存,所以初始化加一
    public static void main(String[] args) {
        long startTime=System.currentTimeMillis();
        String pathname="files/myfile2.txt";
        FileReader fr;
        GoodsNum2 ob=new GoodsNum2();
        long sum=0;
        try {
            fr = new FileReader(new File(pathname));
            BufferedReader br=new BufferedReader(fr);//按行读文件
            String str="";
            while ((str=br.readLine())!=null) {
                String []result=str.split(" ");
                int goodsNumOrPrice=Integer.parseInt(result[1]);
                int goodsPrice=Integer.parseInt(result[2]);
                if(str.startsWith("up")){
                    ob.modyValue(goodsPrice, goodsNumOrPrice);
                }
                else if (str.startsWith("down")) {
                    ob.modyValue(goodsPrice, (-goodsNumOrPrice));
                }
                else if (str.startsWith("query")){
                    sum+=ob.calSum(goodsPrice)-ob.calSum(goodsNumOrPrice-1);
                }
                else {
                    System.out.println("wrong operation!"+str);
                    break;
            }

            }
            System.out.println("sum="+sum);
            System.out.println((System.currentTimeMillis()-startTime)+"ms");
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
    public int lowbit(int t){
        return t&(t^(t-1));
    }

    public void modyValue(int pos,int value){
        while (pos<=arr_len) {
            C[pos]+=value;
            pos+=lowbit(pos);
        }
    } 

    public int calSum(int end){
        int sum=0;
        while (end>0) {
            sum+=C[end];
            end-=lowbit(end);
        }
        return sum;
    }
}

结果:

Java树形列表加分页_数组_03