学习资源:慕课网liyubobobo老师的《玩儿转数据结构》
1、简介
- 线段树是一种二叉搜索树,它将一个大的区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
- 对于线段树中的每一个非叶子结点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子结点数目为N,即整个线段区间的长度。
- 使用线段树可以快速的查找某一个结点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,因此有时需要离散化让空间压缩。
- 线段树不一定是满的二叉树;线段树不是完全二叉树;线段树是平衡二叉树
- 线段树的使用场景一般是查询,所以线段树所作用的区间本身是固定的。
2、线段树的实现
2.1、线段树的底层实现
线段树是平衡二叉树,内部可以使用数组表示。
我们可以将线段树作为一棵满的二叉树,不存在的结点看作是空即可。
一个长度为n的数据集合,在线段树中总可以使用一个长度为4n的数组容纳。
2.2、融合器Merger
线段树中的结点部分是单个的元素,部分是null结点,剩余的都是一个个的融合后的结点,那么如何表示这样的结点呢?应该视具体的业务而定,这里创建一个融合器接口,传入两个结点,融合为一个结点。
public interface Merger<E> {
E merge(E a, E b);
}
2.3、基础部分代码
public class SegmentTree<E> {
private E[] data;
private E[] tree;
private Merger<E> merger;
public E get(int index){
if(index<0 || index>=data.length){
throw new IllegalArgumentException("索引不合法");
}
return data[index];
}
public int getSize(){
return data.length;
}
// 完全二叉树中,当前结点的左孩子结点所在的索引
private int leftChild(int index){
return 2*index + 1;
}
// 完全二叉树中,当前结点的左孩子结点所在的索引
private int rightChild(int index){
return 2*index + 2;
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append('[');
double tier = 1.0;
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(", ");
if(i == Math.pow(2.0, tier)-2){
res.append("
");
tier++;
}
}
res.append(']');
return res.toString();
}
}
2.4、创建线段树
// 构造器。参数1:传入的数据集合;参数2:融合器
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];
// 参数1:当前创建的线段树的根结点的索引;参数2:当前结点所代表的线段,根结点就是0~最后一个
buildSegmentTree(0, 0, data.length - 1);
}
// 递归创建
private void buildSegmentTree(int treeIndex, int l, int r) {
// 递归到底,叶子结点,直接return
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]);
}
2.5、区间查询
// 返回区间[queryL, queryR]的值
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.");
// 参数1:当前查询的线段树的根节点;参数2:当前结点所代表的线段,根结点就是0~最后一个
return query(0, 0, data.length - 1, queryL, queryR);
}
// 在以treeIndex为根的线段树中[l...r]的范围里,搜索区间[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 rightResult = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);
return merger.merge(leftResult, rightResult);
}
2.6、更新元素
在线段树中更新某个元素,不只需要更新线段树(完全二叉树)中的单个的结点,还要向上追溯更新其父辈结点。
public void set(int index, E e){
if(index<0 || index>=data.length){
throw new IllegalArgumentException("索引不合法");
}
// 更新私有的data数组
data[index] = e;
// 再更新tree数组
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]);
}
2.7、其他操作
- 更新区间
- 二维线段树
- 动态线段树
3、测试
具体测试的时候,需要传入出一个融合器。(可以使用Lambda表达式)
@Test
public void test(){
Integer[] nums = {-2,0,3,-5,2,-1};
SegmentTree<Integer> segmentTree = new SegmentTree<Integer>(nums, new Merger<Integer>() {
@Override
public Integer merge(Integer a, Integer b) {
return a + b;
}
});
System.out.println(segmentTree);
System.out.println(segmentTree.getSize());
System.out.println(segmentTree.get(3));
System.out.println(segmentTree.query(2, 5));
}