文章目录
为什么要使用线段树?
对于有一类问题,我们关心的是线段(或者区间)
最经典的线段树问题:区间染色
有一面墙,长度为 n n n,每次选择一段墙进行染色, m m m次操作后,可以看见多少种颜色? m m m次操作后,可以在区间 [ i , j ] [i,j] [i,j]内看见多少种颜色?
使用数组实现 | |
---|---|
染色操作(更新区间) | O ( n ) O(n) O(n) |
查询操作(查询区间) | O ( n ) O(n) O(n) |
另一类经典问题:区间查询,查询一个区间 [ i , j ] [i,j] [i,j]的最大值,最小值,或者区间数字和
实质:基于区间的统计查询
例如,对于某一电商网站,查询2021年注册用户中消费最高的用户?消费最小的用户?学习时间最长的用户?
使用数组实现 | 使用线段树实现 | |
---|---|---|
更新 | O ( n ) O(n) O(n) | O ( l o g n ) O(logn) O(logn) |
查询 | O ( n ) O(n) O(n) | O ( l o g n ) O(logn) O(logn) |
将线段树抽象出来,就是解决如下问题:对于给定区间
- 更新:更新区间中一个元素或者一个区间的值
- 查询:查询一个区间 [ i , j ] [i,j] [i,j]的最大值、最小值或者区间和
什么是线段树(SegmentTree)?
线段树,也称区间树,是一种二叉搜索树,线段树的每个结点都存储了一个区间
综上,如果区间有 n n n个元素,用数组表示线段树需要 4 n 4n 4n的空间(估计值,有冗余,如下图所示,也可考虑使用链表的方式实现不会有空间浪费),不考虑添加元素,即区间固定,使用 4 n 4n 4n的静态空间即可。
创建线段树
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];
}
// 开辟 4n 空间
tree = (E[])new Object[4 * arr.length];
// 采用递归创建线段树
buildSegmentTree(0, 0, data.length - 1);
}
/**
* 在treeIndex的位置创建表示区间[l,r]的线段树
* @param treeIndex 根节点
* @param l 左边界
* @param r 右边界
*/
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);
// 两个数据如何融合 取决于merge
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];
}
/**
* 返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
* @param index
* @return
*/
private int leftChild(int index) {
return 2*index+1;
}
/**
* 返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
* @param index
* @return
*/
private int rightChild(int index) {
return 2*index+2;
}
@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();
}
public static void main(String[] args) {
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;
// }
// });
// 测试创建线段树,以求和为例
SegmentTree<Integer> segmentTree = new SegmentTree<Integer>(nums, (a,b)->a+b);
System.out.println(segmentTree.toString());
}
}
/**
* 用于线段树的具体逻辑/业务
* @author yzze
* @create 2020-05-10 16:01
*/
public interface Merger<E> {
E merge(E a, E b);
}
线段树的查询
/**
* 区间查询
* 返回区间[qL, r]的值
* @param qL
* @param qR
* @return
*/
public E query(int qL, int qR) {
// 边界检查
if (qL < 0 || qL >= data.length ||
qR < 0 || qR >= data.length || qL > qR) {
throw new IllegalArgumentException("Index is illegal.");
}
return query(0, 0, data.length-1, qL, qR);
}
/**
* 在以treeIndex为根的线段树的[l,r]范围里,搜索区间[qL,qR]的值
* @param treeIndex
* @param l
* @param r
* @param qL
* @param qR
* @return
*/
private E query(int treeIndex, int l, int r, int qL, int qR) {
if (l == qL && r == qR) {
return tree[treeIndex];
}
int mid = l + (r - l) / 2;
int leftTreeIndex = leftChild(treeIndex);
int rightTreeIndex = rightChild(treeIndex);
if (qL >= mid+1) {
return query(rightTreeIndex, mid+1, r, qL, qR);
} else if (qR <= mid) {
return query(leftTreeIndex, l, mid, qL, qR);
}
E leftResult = query(leftTreeIndex, l, mid, qL, mid);
E rightResult = query(rightTreeIndex, mid+1, r, mid+1,qR);
return merger.merge(leftResult, rightResult);
}
线段树中的更新操作
/**
* 将Index位置的值更新为e
* @param index
* @param e
*/
public void set(int index, E e) {
if (index < 0 || index >= data.length) {
throw new IllegalArgumentException("Index is illegal.");
}
data[index] = e;
set(0, 0, data.length-1, index, e);
}
// 在以treeIndex为根的线段树中更新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]);
}
More
线段树相关问题
-
Acwing 245. 你能回答这些问题吗?
-
LeetCode 303. 区域和检索 - 数组不可变
-
LeetCode 307. 区域和检索 - 数组可修改
Reference
-
线段树
-
线段树
-
玩转数据结构