线段树 : 它在各个节点保存一条线段(数组中的一段子数组),主要用于高效解决连续区间的动态查询问题,由于二叉结构的特性,它基本能保持每个操作的复杂度为O(logn)
。
线段树的每个节点表示一个区间,子节点则分别表示父节点的左右半区间,例如父亲的区间是[L,R]
,那么(m=(L+R)/2
)左儿子的区间是[L,m]
,右儿子的区间是[m+1,R]
。
[L,m]
和[m+1,R]
,然后递归的去创建各自的线段树;(1) 第一种情况: 我们的元素全部落在最后一层,这样的话我们大概只需要2*n
(n
是数组,也是最后一层的元素的个数)。
(2) 第二种情况: 我们的元素不全部是在最后一层,而是在倒数第二层也有:这样的话我们最多可能需要4*n
的空间。
上面的第二种情况就是我们的数组元素有在倒数第二层的情况: 例如下面的例子,当区间的划分的个数是奇数个的时候,势必左右两边的个数不同,下面的图是左边比右边的少一个,也就是左边区间是[L,m-1]
,右边的区间是[m,R]
(但是我在代码实现的时候,左边是[L,m]
,右边是[m+1,R]
,也就是左边多一个)。
我们保存树的结构是类似和堆一样的使用数组来保存,使用下标来对应左右孩子:
public class SegmentTree<E> {
//操作的方式: 求和 | 查询最大值 | 最小值
private interface Merger<E>{
E merge(E a,E b);
}
private E[] tree;
private E[] data;
private Merger<E> merger;
public SegmentTree(E[] arr,Merger 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]; //最多需要4 * n
buildSegmentTree(0, 0, arr.length - 1);
}
}
注意其中:
Merger
表示的处理方式,比如查询区间和,查询最大值,查询最小值。data
用来保存用户传进来的arr
值,是它的拷贝。tree
就是用数组来描述树的结构,注意大小为4 * arr.length
。buildSegmentTree()
函数是创建线段树。然后就是对线段树的创建:
要注意的是:
[L,m]
,右区间[m+1,R]
,当然也可以右区间多一个元素; // tree是树的结构(类似堆的存储)
public void buildSegmentTree(int treeIndex,int L,int R){
if( L == R){//叶子结点 直接创建赋值
tree[treeIndex] = data[L];
return;
}
int treeL = treeIndex * 2 + 1; //左孩子对应的下标
int treeR = treeIndex * 2 + 2; //右孩子下标
int m = L + (R - L) / 2; //
// 先把左右子树给我建好
//[0,4] ---> [0,2](3), [2,4](2)
buildSegmentTree(treeL,L,m);
buildSegmentTree(treeR,m+1,R);
//然后我再把左右子树合并(sum | max | min)
tree[treeIndex] = merger.merge(tree[treeL],tree[treeR]);
}
qR <= m
,说明我们要去左边的区间查询;qL > m
,说明我们要去右边的区间查询; //查询[qL,qR]的 sum | max | min
public E query(int qL,int qR){
if(qL < 0 || qL >= data.length || qR < 0 || qR >= data.length || qL > qR)return null;
return query(0,0,data.length - 1,qL,qR);
}
// [treeIndex,L,R]表示的是结点为treeIndex的树的左右区间范围(arr的下标)
private E query(int treeIndex,int L,int R,int qL,int qR){
if(L == qL && R == qR){
return tree[treeIndex];
}
int m = L + (R - L) / 2;
int treeL = treeIndex * 2 + 1;
int treeR = treeIndex * 2 + 2;
if(qR <= m){ //和右区间没关系 ,直接去左边查找 [0,4] qR <= 2 [0,2]之间查找
return query(treeL,L,m,qL,qR);
}else if(qL > m ) {//和左区间没有关系,直接去右边查找 [0,4] qL > 2 --> [3,4]
return query(treeR,m+1,R,qL,qR);
}else { //在两边都有,查询的结果 合并
return merger.merge(query(treeL,L,m,qL,m), //注意是查询 [qL,m]
query(treeR,m+1,R,m+1,qR)); //查询[m+1,qR]
}
}
线段树的更新也是类似的,首先修改数组的值,然后递归的查找到叶子,然后沿途修改树中结点的值即可。
public void update(int index,E e){
if(index < 0 || index >= data.length )return;
data[index] = e; //首先修改data
update(0,0,data.length-1,index,e);
}
private void update(int treeIndex,int L,int R,int index,E e){
if(L == R){
tree[treeIndex] = e;
return;
}
int m = L + (R - L ) / 2;
int treeL = 2 * treeIndex + 1;
int treeR = 2 * treeIndex + 2;
if(index <= m){ //左边
update(treeL,L,m,index,e);
}else {
update(treeR,m+1,R,index,e);
}
tree[treeIndex] = merger.merge(tree[treeL],tree[treeR]); //更新完左右子树之后,自己受到影响,重新更新和
}
import java.util.Arrays;
public class SegmentTree<E> {
//操作的方式: 求和 | 查询最大值 | 最小值
private interface Merger<E>{
E merge(E a,E b);
}
private E[] tree;
private E[] data;
private Merger<E> merger;
public SegmentTree(E[] arr,Merger 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]; //最多需要4 * n
buildSegmentTree(0, 0, arr.length - 1);
}
// tree是树的结构(类似堆的存储)
public void buildSegmentTree(int treeIndex,int L,int R){
if( L == R){
tree[treeIndex] = data[L];
return;
}
int treeL = treeIndex * 2 + 1;
int treeR = treeIndex * 2 + 2;
int m = L + (R - L) / 2;
// 先把左右子树给我建好
//[0,4] ---> [0,2](3), [2,4](2)
buildSegmentTree(treeL,L,m);
buildSegmentTree(treeR,m+1,R);
//然后我再把左右子树合并(sum | max | min)
tree[treeIndex] = merger.merge(tree[treeL],tree[treeR]);
}
//查询[qL,qR]的 sum | max | min
public E query(int qL,int qR){
if(qL < 0 || qL >= data.length || qR < 0 || qR >= data.length || qL > qR)return null;
return query(0,0,data.length - 1,qL,qR);
}
// [treeIndex,L,R]表示的是结点为treeIndex的树的左右区间范围(arr的下标)
private E query(int treeIndex,int L,int R,int qL,int qR){
if(L == qL && R == qR){
return tree[treeIndex];
}
int m = L + (R - L) / 2;
int treeL = treeIndex * 2 + 1;
int treeR = treeIndex * 2 + 2;
if(qR <= m){ //和右区间没关系 ,直接去左边查找 [0,4] qR <= 2 [0,2]之间查找
return query(treeL,L,m,qL,qR);
}else if(qL > m ) {//和左区间没有关系,直接去右边查找 [0,4] qL > 2 --> [3,4]
return query(treeR,m+1,R,qL,qR);
}else { //在两边都有,查询的结果 合并
return merger.merge(query(treeL,L,m,qL,m), //注意是查询 [qL,m]
query(treeR,m+1,R,m+1,qR)); //查询[m+1,qR]
}
}
public void update(int index,E e){
if(index < 0 || index >= data.length )return;
data[index] = e; //首先修改data
update(0,0,data.length-1,index,e);
}
private void update(int treeIndex,int L,int R,int index,E e){
if(L == R){
tree[treeIndex] = e;
return;
}
int m = L + (R - L ) / 2;
int treeL = 2 * treeIndex + 1;
int treeR = 2 * treeIndex + 2;
if(index <= m){ //左边
update(treeL,L,m,index,e);
}else {
update(treeR,m+1,R,index,e);
}
tree[treeIndex] = merger.merge(tree[treeL],tree[treeR]); //更新完左右子树之后,自己受到影响,重新更新和
}
public static void main(String[] args) {
int[] nums = {-2, 0, 3, -5, 2, -1};
Integer[] arr = new Integer[nums.length];
for(int i = 0; i < nums.length; i++) arr[i] = nums[i];
SegmentTree<Integer>segmentTree = new SegmentTree<Integer>(arr, new Merger<Integer>() {
@Override
public Integer merge(Integer a, Integer b) {
return a + b;
}
});
System.out.println(segmentTree.query(0, 2));
System.out.println(Arrays.toString(segmentTree.tree));
segmentTree.update(1,2);
System.out.println(segmentTree.query(0, 2));
System.out.println(Arrays.toString(segmentTree.tree));
}
}
知道了上面的操作,这个题目完全就是上面的操作的结合:
private interface Merger<E> {
E merge(E a, E b);
}
private class SegmentTree<E> {
private E[] tree;
private E[] data;
private Merger<E> merger;
public SegmentTree(E[] arr,Merger 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]; //最多需要4 * n
buildSegmentTree(0, 0, arr.length - 1);
}
public void buildSegmentTree(int treeIndex, int L, int R) {
if (L == R) {
tree[treeIndex] = data[L];
return;
}
int treeL = treeIndex * 2 + 1;
int treeR = treeIndex * 2 + 2;
int m = L + (R - L) / 2;
buildSegmentTree(treeL, L, m);
buildSegmentTree(treeR, m + 1, R);
tree[treeIndex] = merger.merge(tree[treeL], tree[treeR]);
}
public E query(int qL, int qR) {
if (qL < 0 || qL >= data.length || qR < 0 || qR >= data.length || qL > qR) return null;
return query(0, 0, data.length-1, qL, qR);
}
private E query(int treeIndex, int L, int R, int qL, int qR) {
if (L == qL && R == qR) {
return tree[treeIndex];
}
int m = L + (R - L) / 2;
int treeL = treeIndex * 2 + 1;
int treeR = treeIndex * 2 + 2;
if (qR <= m) { //和右区间没关系 ,直接去左边查找 [0,4] qR <= 2 [0,2]之间查找
return query(treeL, L, m, qL, qR);
} else if (qL > m) {//和左区间没有关系,直接去右边查找 [0,4] qL > 2 --> [3,4]
return query(treeR, m + 1, R, qL, qR);
} else { //在两边都有,查询的结果 合并
return merger.merge(query(treeL, L, m, qL, m), //注意是查询 [qL,m]
query(treeR, m + 1, R, m + 1, qR)); //查询[m+1,qR]
}
}
public void update(int index,E e){
if(index < 0 || index >= data.length )return;
data[index] = e; //首先修改data
update(0,0,data.length-1,index,e);
}
private void update(int treeIndex,int L,int R,int index,E e){
if(L == R){
tree[treeIndex] = e;
return;
}
int m = L + (R - L ) / 2;
int treeL = 2 * treeIndex + 1;
int treeR = 2 * treeIndex + 2;
if(index <= m){ //左边
update(treeL,L,m,index,e);
}else {
update(treeR,m+1,R,index,e);
}
tree[treeIndex] = merger.merge(tree[treeL],tree[treeR]); //更新完左右子树之后,自己受到影响,重新更新和
}
}
private SegmentTree<Integer> segTree;
public NumArray(int[] nums) {
if(nums == null || nums.length == 0)return;
Integer[] arr = new Integer[nums.length];
for(int i = 0; i < nums.length ; i++) arr[i] = nums[i];
segTree = new SegmentTree<Integer>(arr, new Merger<Integer>() {
@Override
public Integer merge(Integer a, Integer b) {
return a + b;
}
});
}
public void update(int i, int val) {
if(segTree == null)return;
segTree.update(i,val);
}
public int sumRange(int i, int j) {
return segTree.query(i,j);
}