线段树
线段树(英语:Segment tree)是一种二叉树形数据结构,1977年由Jon Louis Bentley发明[1],用以储存区间或线段,并且允许快速查询结构内包含某一点的所有区间。
线段树常常用对区间数据的更新和查询,主要的作用就是体现在对区间的处理。
最常见就是这样一个问题:
现在有一个长度为nn的数组a[i]a[i],给定一个区间ii~jj,返回这个区间的元素a[i]a[i]~a[j]a[j]的和sumsum。
最直接的方式便是对该区间的元素直接累加,这样的时间复杂度为O(j−i+1)O(j−i+1);然而还有一种简单的方式,可以将复杂度降为O(1)O(1),但是这种方式需要预处理一下原数组。
首先令sum[i]sum[i]表示区间00~ii之间的元素的和,预处理的时候遍历原数组得到这个sumsum数组,
然后,对于每一次区间和的查询ii,jj,则通过sum=sum[j]−sum[i−1]sum=sum[j]−sum[i−1]等式直接计算得出。
上述第二种方式固然高效,但是当我们修改了原数组的某一个元素a[k]a[k],那么sumsum数组也要相应的被修改,且修改的时间复杂度为O(n)O(n)。那么是否存在更好的算法,能把这个O(n)O(n)优化到更低呢?
以上的问题属于典型的单点更新,区间查询的问题,此类问题很适合使用线段树这个数据结构来解决,线段树可以将上面由于某个元素更新,而需要更新sumsum数组的O(n)O(n),降低到O(logn)O(logn)。
线段树将原来数组的nn个区间(每个元素构成一个区间),划分成了更多的区间(约4∗n4∗n个区间),每个区间作为一棵二叉树的节点,且满足如下条件:
- 叶节点的区间大小为1
- 同一层的区间的交集为空
- 父节点的区间等于左孩子节点代表的区间与右孩子代表的区间的并集
因此,线段树其实是一棵二叉树,大致的形状如下:
对于单点修改来说:
因为对单点进行更新后,需要对包含该点的区间进行更新,因此我们希望包含该点的区间尽量少,这样在单点更新后,需要更新的区间就少了很多。通过这种同层的区间交集为空,父节点区间是孩子节点区间的并集的方式,包含某个点的区间数就降为了二叉树的高度O(logn)O(logn),从而将时间复杂度降下来了。
假设把 a[6]+=7 ,看看哪些区间需要修改?[6],[5,6],[5,7],[1,7],[1,13]这些区间全部都需要+7.其余所有区间都不用动。
对于区间查询来说:
给定一个区间ii~jj,由于节点区间是孩子节点区间的并集的性质,我们可以从下向上不断的合并区间,这样查询的时间复杂度也是树高O(logn)O(logn)。
下图是对区间22~1212进行查询的过程:
因此通过线段树,对于这种单点修改和区间查询的时间复杂度都能为降低到O(logn)O(logn),通过上面的图示,可以发现两种操作都可以使用简洁的递归代码来实现。
代码实现
public class SegmentTree {
public static void main(String[] args) {
// TODO Auto-generated method stub
int[] A = new int[]{1,2,3,4,5,6,7,8,9};
SegmentTree demo = new SegmentTree(A);
System.out.println(demo.query(0, 9));
demo.update(0, 1000);
System.out.println(demo.query(0, 9));
}
int[] A = null;
int[] sum = null;
public SegmentTree(int[] A) {
// TODO Auto-generated constructor stub
this.A = A;
sum = new int[A.length << 2];//sum数组大小为原数组的4倍,这个值是根据满二叉树计算出来的;
//buildTree
buildSegmentTree(0, A.length-1, 0);
}
//构建线段树
public void buildSegmentTree(int l, int r, int idx) {
//base case
if (l == r) {
sum[idx] = A[l];
return;
}
//递归创建左右线段树
int m = (l+r) >> 1;
buildSegmentTree(l, m, ((idx+1) << 1) - 1);//2*(rt+1) - 1
buildSegmentTree(m+1, r, (idx+1) << 1);//2*(rt+1)
sum[idx] = sum[((idx+1) << 1) - 1] + sum[(idx+1) << 1];
}
//区间查询
public int query(int L, int R) {
return query(L, R, 0, A.length-1, 0);
}
private int query(int L, int R, int l, int r, int idx) {
//base case
if (L <= l && r <= R) return sum[idx];
int m = (l + r) >> 1;
int s = 0;
if (L <= m) s += query(L, R, l, m,((idx+1) << 1) - 1);
if (R >= (m + 1)) s += query(L, R, m+1, r, (idx+1) << 1);
return s;
}
//单点更新
public void update(int idx, int C) {
update(idx, C, 0, A.length-1, 0);
}
private void update(int L,int C, int l, int r, int idx) {
if (l == r) {
sum[idx] += C;
return;
}
int m = (l+r) >> 1;
//判断需要修改的点L落在左子树还是右子树
if (L <= m) {
update(L, C, l, m, ((idx+1) << 1) - 1);
} else {
update(L, C, m+1, r, (idx+1) << 1);
}
//更新当前sum数组的idx节点
sum[idx] = sum[((idx+1) << 1) - 1] + sum[(idx+1) << 1];
}
}
除此之外,树状数组和线段树类似,也能完成一些线段树的功能,但是两者存在一些差异,关于树状数组后面继续总结。