SegmentTree叫线段树,也叫区间树。在某些情况下,只关注某一段区间的数据情况,比如经典的区间染色问题。查询和更新某区间的信息使用数组实现时间复杂度都是O(n),用线段树实现查询和更新时间复杂度为O(log n),线段树更新数组中一个值或者一个区间的值,查询一个区间的最大值、最小值或者是求和等操作。线段树是主要是将传入的数组形成树结构记录区间信息,如下图所示结构
对于元素个数不是2 ^ 2,其结构是如下图所示,不停的二分区间
对于满二叉树有h层总共有2 ^ h - 1个结点, 每一层节点数为 2 ^ (h -1) 。如果数组数据容量为n = 2 ^ k , 那么对于相应的线段树容量为2n空间,最坏情况 数组n = 2 ^ k + 1,需要开辟的空间为4n。线段树不考虑添加元素,区间固定,使用4n的静态空间即可,用这4n空间的数组表示树结构。其中每个空间的内容定义了一个Merger融合器接口用于自定义数据融合方式,比如求和等操作。
1、Merger接口
public interface Merger<E> {
E merge(E a, E b); //融合方法,需要自定义实现
}
2、SegmentTree类
public class SegmentTree<E> {
private E[] tree; //声明树数组,盛放区间数据
private E[] data; //数据数组,盛放原始数据
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[4 * arr.length];
buildSegmentTree(0, 0, data.length - 1);
}
//构建线段树函数,传入树索引位置,区间左右位置,整个过程采取递归思想
private 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]);
}
public int getSize() { //获取数据数组大小
return data.length;
}
public E get(int index) { //根据索引获取数据数组的值
if (index < 0 || index >= data.length)
throw new IllegalArgumentException("Index is illegal.");
return data[index];
}
private int leftChild(int index) { //根据索引获取左孩子索引
return 2 * index + 1;
}
private int rightChild(int index) { //根据索引获取右孩子索引
return 2 * index + 2;
}
//查询某一个区间信息
public E query(int queryL, int queryR) {
//判断索引合法性
if (queryL < 0 || queryL >= data.length || queryR < 0 || queryR >= data.length || queryL > queryR)
throw new IllegalArgumentException("Index is illegal.");
//调用私有递归查询函数
return query(0, 0, data.length - 1, queryL, queryR);
}
//根据树数组索引, 区间左右位置标定点, 需要查找区间位置来查询区间信息
private E query(int treeIndex, int l, int r, int queryL, int queryR) {
//递归终止条, 左区间等于左查找位置,右区间等于右查找位置,将树中数据返回给上一层
if (l == queryL && r == queryR)
return tree[treeIndex];
int mid = l + (r - l) / 2; //求取区间中间标定位置
int leftTreeIndex = leftChild(treeIndex); //获取左孩子索引
int rightTreeIndex = rightChild(treeIndex); //获取右孩子索引
if (queryL >= mid + 1) { //如果左查找点大于区间中点,则在右区间递归查找
return query(rightTreeIndex, mid + 1, r, queryL, queryR);
} else if (queryR <= mid) { //如果右查找点小于区间中点,则在左区间递归查找
return query(leftTreeIndex, l, mid, queryL, queryR);
}
//如果分布于左右都有,则分别将左边部分和右边都找到,最后融合返回给上一层
E leftResult = query(leftTreeIndex, l, mid, queryL, mid);
E rightRsult = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);
return merger.merge(leftResult, rightRsult);
}
public void set(int index, E e) { //根据索引修改数据数组的值
//判断索引的合法性
if (index < 0 || index >= data.length)
throw new IllegalArgumentException("Index is illegal");
//将索引位置元素更新为e
data[index] = e;
//调取递归元素将树中区间信息更新
set(0, 0, data.length - 1, index, e);
}
//递归更新区间信息
private void set(int treeIndex, int l, int r, int index, E e) {
if (l == r) {
tree[treeIndex] = e;
return;
}
int mid = l + (r - l) / 2;
int leftTreeIndex = leftChild(treeIndex);
int rightTreeIndex = rightChild(treeIndex);
if (index >= mid + 1)
set(rightTreeIndex, mid + 1, r, index, e);
else
set(leftTreeIndex, l, mid, index, e);
tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
}
@Override
public String toString() {
StringBuilder res = new StringBuilder();
res.append("[ ");
for (int i = 0; i < tree.length; i++) {
if (tree[i] != null) {
res.append(tree[i]);
} else {
res.append("null");
}
if (i != tree.length - 1) {
res.append(", ");
}
}
res.append(" ]");
return res.toString();
}
}
以上就是线段树的实现过程,线段树缺点是耗费空间较大,优点是区间查找信息更方便。