树是数据结构中最重要的逻辑结构。其中有红黑树,伸展树,AVL树,BST树,2-4树,B树,B+树,B-树等等。这里我们介绍一种新的树状结构--线段树。线段树常常用来求任意下标元素的最大值、最小值或者是求和等等。线段树构造有很多方式,比如:
图片来源:线段树
这个首先用树的结构将每个节点信息存储下来。大概有开始编号,结束编号,父节点指针,左孩子指针,右孩子指针,以及求和的值。那么更新的时候只需要从当前节点一直上溯到根节点就可以,这样时间复杂度是O(logn).在求下标值的时候,依据节点的开始和结束编号进行查找即可,这样时间复杂度也是O(logn).这个方法的认为不太好的地方在于构造一个树要耗较多内存,而且相对来说不容易建。
那么是不是可以利用数组来实现树的操作呢,答案是肯定的。在数据结构中,很多高级或者说复杂的结构其内部都是用数组实现的。接下来将介绍关于线段树存储的树状数组。(下面的概念中二进制位数是从右往左,开始下标为0,也记倒数第零位。树的层数从下往上,开始下标也是0,也记第零层。还有覆盖孩子节点,其中孩子节点相对来说比较宽泛,是在覆盖当前节点之下的那些节点)
图片来源:线段树
这样的树构造方式和上面的不太一样,但是却是十分有用。其中A[1]到A[8]是原给定数组的下标从0到7对应的数。其中C[i]=A[i-2^k+1]到A[i]之间元素的和。可以看出将每个i(i>=1)写成二进制形式,恰好第零层是二进制倒数第零位是1;第一层恰好是二进制倒数第一位是1;依次类推,第k层应该是二进制倒数第k位是1的。注意这里的层数是从下往上的。
这样还有个好处就是,当找当前节点的父节点的时候只需要在当前节点i上加上2^k,这里k是当前节点所在的层次。为什么这样能得到父节点的下标呢?因为当前层下标的二进制倒数第k位是1,而倒数j位都是0(这很显然,如果不是那么当前节点应该在j层,其中倒数第j位为1.这里j<k).那么当前节点i+2^k,相当于将倒数第k位上的1变成了0,就往上移动了一层/或者多层,从而达到其上面的父节点下标。(update)
当找前一颗树的时候只需要将当前节点下标i减去2^k,这样就和上面类似,层数上升了,因为倒数第k位的1变成了0,但是由于上面是加使得值变大而到达父节点下标,那么这里的减使得下标值减少,则会使得到达前一颗树的下标位置。比如6(00..0110)在当倒数第一位的1变成0(因为6在第一层),由于前面还有1,因此变化为4,也就是到达了左边的一颗树的下标处。这样4(00..0100)的倒数第二位的1变成0(因为4在第二层)后结果变成0.当减少到0的时候结束。因此sum[6]=C[6]+C[4].其中sum[i]表示原给定数组下标从0到i-1的求和。如果这样不好理解,可以将第二幅图进行转化。
图片来源:线段树
从第一层开始将左孩子进行翻转,构成一颗完全二叉树。每个子树(即以当前节点为根节点的一颗树)包含(或者说覆盖)有2^k(k是当前节点的层次)个孩子,那么其余没有覆盖的应该是当前节点的下标i-已经覆盖的孩子节点。比如6在第1层,覆盖了2^1个孩子,那么还有4个孩子没有覆盖,因此下标移到6-2^1=4的位置处。到4的时候,由于其在第二层,因此覆盖了2^2个孩子,那么下标移到4-2^2=0这样所有的孩子都覆盖到了,也就是实现了原给定数组从下标0开始到下标i-1的元素的和。即sums[6]=C[6]+C[4].(sum)
这样就可以写出线段树的三个基本函数。
1.求2^k
private int bit(int i){
return i&(-i);//返回i^k,其中k是二进制i的最后不为0的位置,比如i=4,二进制为00...0100,最后不为0的位置是2
}
2.更新
private void update(int i,int val){
while(i<C.length){
C[i]+=val;
i+=bit(i);//上朔,参考前面update段落
}
}
3.求和
private int sum(int i){
int sum=0;
while(i>0){
sum+=C[i];
i-=bit(i);//找前一颗树,参考前面sum段落
}
return sum;
}
Reference: