线段树(segment tree)详解
什么是线段树
线段树是一棵平衡搜索树,但是不是完全二叉树,其实也是一棵二分搜索树,它储存的是一个区间的信息。
每个节点以结构体的方式存储,结构体包含以下几个信息:
区间左端点、右端点;(这两者必有)
区间内要维护的信息(实际情况而定,数目不等)。
一个具体的线段树如下所示,这里是一数组作为底层数据结构来阐述线段树,所以就简单的将线段树看为满二叉树,这样的话肯定是会造成空间的浪费,但是这里就是为了理解线段树这种数据结构,不去考虑其更高层的实现,要想空间不浪费的话就动态的创建线段树就可以了。
构建二叉树
其实只要涉及到树的一些操作就避免不了使用递归的思想来创建树,以为每一棵树都是有一棵棵子树构成的.
主体的思路就是:
- 对于二分到的每一个结点,给它的左右端点确定范围。
- 如果是叶子节点,存储要维护的信息。
- 子树合并。
public class SegmentTree<E> {
private E[] data;
private E[] tree;
private Merger<E> merger;
public SegmentTree(E[] arr, Merger<E> merger){
this.merger = merger;
data = (E[]) new Object[arr.length];
for (int i = 0; i < arr.length; i++){
data[i] = arr[i];
}
tree = (E[])new Object[arr.length * 4 ]; //为保证树是一个满二叉树,包含n个节点最差的情况需要的空间是4n的,所以其中肯定有为空的节点
//其实线段是一颗平衡二叉树,它是一棵空树或它的左右两个子树的高度差的绝对值不超过1
buildSegmentTree(0,0,arr.length-1); //创建线段树
}
/**
* 创建表示data区间[l...r]的线段树
* @param treeIndex 以treeIndex为起点
* @param L L
* @param R R
*/
public void buildSegmentTree(int treeIndex, int L, int R){
if ( L == R){
tree[treeIndex] = data[L];
return;
}
int leftTreeIndex = leftChild(treeIndex);
int rightTreeIndex = rightChild(treeIndex);
int mid = L + (R - L)/2;
buildSegmentTree(leftTreeIndex, L, mid);
buildSegmentTree(rightTreeIndex, mid + 1, R);
tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
}
**
* 返回左孩子节点的索引值
* @param index
* @return
*/
public int leftChild(int index){
return 2 * index + 1;
}
/**
* 返回右孩子节点的索引值
* @param index
* @return
*/
public int rightChild(int index){
return 2 * index + 2;
}
上面初始化树的空间的时候使用的空间大小是要存储的数组的大小的4倍,具体是怎么来的?
对于一棵满二叉树,其第h层的元素有2^h-1个节点,并且第h层元素的节点的个数等于前1—>h-1
层元素个数的总和。那么来看存储n个元素的时候需要多大的空间?需要4n的空间,具体如下图:
这里实现了Merger的接口,这个结接口就是用来应对对线段树的不同操作而设置的,用户可以根据自己想要实现的功能自己进行这个接口的具体实现。
public interface Merger<E> {
E merge(E a, E b);
}
树的查询操作
查询一个线段树其实使用递归的思想来实现,例如说我们要查询[2,5]这个区间内的元素,然后从根节点开始递归,[2,5]是在[0,7]之内的,然后求出根的左右孩子和中间分割点mid,查询区间的左右边界与mid比较,然后根据其满足的条件看需要在左子树还是右子树继续查询,不断递归直到找到要查询的区间。说的有点啰嗦了,具体看下面:
/**
* 返回区间[queryL, queryR]的值
* @param queryL queryL
* @param queryR queryR
* @return
*/
public E query(int queryL, int queryR){
return query(0,0,data.length-1, queryL,queryR);
}
/**
* 以treeIndex为根的线段树中[l...r]的范围里,搜索区间[queryL...queryR]的值
* @param index
* @param L
* @param R
* @param queryL
* @param queryR
* @return
*/
public E query(int index, int L, int R, int queryL, int queryR){
if (queryL < 0 || queryR < 0|| queryL