线段树(SegmentTree)也叫区间树
最经典的线段树问题:区间染色,区间查询
比如说统计某电商网站2017年的消费最高的,这个问题其实是简单的
但是我先看的是2017年至现在的消费情况,它的数据一直是变得,那么线段树是一个不错的选择
它的数据是不断更新的,不断查询的,也可以使用数组实现
我们不考虑在线段树中添加或删除元素,我们所解决的问题是区间本身是固定的,只是区间中的元素可能发生变化
那么线段树具体是什么样子的呢>
线段树不是完全二叉树
线段树是平衡二叉树
上面这个例子是刚刚存储的数据个数为2^3个
如果说要是存储十个数据会是什么样子呢
线段树的创建:
首先要根据给出的数组构建出一棵线段树,构造中根据自己传入的合并器在构建的时候为要存储的元素的根节点,根节点的根节点....附上值
public interface Merger<E> { E merge(E a, E b); }
/** * 线段树中的元素 */ 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[4 * arr.length]; buildSegmentTree(0, 0, arr.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); //mid变量表示所构建的线段树的中间位置 int mid = l + (r - l) / 2; buildSegmentTree(leftTreeIndex, l, mid); buildSegmentTree(rightTreeIndex, mid + 1, r); //在构建完左右子树的时候,根据业务逻辑为父节点附上值 //业务逻辑 比如说统计某个区间的所有值之和 tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]); }
线段树的查询:
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); } /** * 在以 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); }
线段树的更新操作:
public void set(int index, E e) { if (index < 0 || index >= data.length) { throw new IllegalArgumentException("Index is Illegal"); } 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 rightTreeIIndex = rightChild(treeIndex); if (index >= mid + 1) { set(rightTreeIIndex, mid + 1, r, index, e); } else /*index <= mid*/ { set(leftTreeIndex, l, mid, index, e); } //更新完成之后,其父节点的值也需要更新,父节点的父节点也是... tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIIndex]); }
完成基本操作之后,测试一下编写的方法>
public class Main { public static void main(String[] args) { Integer[] arr = {-2, 0, 3, -5, 2, -1}; SegmentTree<Integer> segTree = new SegmentTree<Integer>(arr, (a, b) -> a + b); System.out.println(segTree.toString()); System.out.println(segTree.query(2, 5)); segTree.set(0, 2); System.out.println(segTree); } }
整段代码:
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[4 * arr.length];
buildSegmentTree(0, 0, arr.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);
//mid变量表示所构建的线段树的中间位置
int mid = l + (r - l) / 2;
buildSegmentTree(leftTreeIndex, l, mid);
buildSegmentTree(rightTreeIndex, mid + 1, r);
//在构建完左右子树的时候,根据业务逻辑为父节点附上值
//业务逻辑 比如说统计某个区间的所有值之和
tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
}
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);
}
/**
* 在以 treeIndex 为根节点的线段树 [l . . . r]中,搜索 区间[queryL . . . queryR] 范围的值
*
* @param treeIndex 线段树根节点
* @param l
* @param r
* @param queryL
* @param queryR
* @return
*/
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);
}
public void set(int index, E e) {
if (index < 0 || index >= data.length) {
throw new IllegalArgumentException("Index is Illegal");
}
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 rightTreeIIndex = rightChild(treeIndex);
if (index >= mid + 1) {
set(rightTreeIIndex, mid + 1, r, index, e);
} else /*index <= mid*/ {
set(leftTreeIndex, l, mid, index, e);
}
//更新完成之后,其父节点的值也需要更新,父节点的父节点也是...
tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIIndex]);
}
/**
* 获取某一个位置的元素
* @param index
* @return
*/
public E get(int index) {
if (index < 0 || index >= data.length) {
throw new IllegalArgumentException("Index is illegal");
}
return data[index];
}
public int getSize() {
return data.length;
}
/**
* 计算出左孩子索引
*
* @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();
}
}