线段树是一种树形的高级数据结构。用于解决数组的某段区间的求最值,某段区间更新单个/所有值等问题(或抽象简化后与之相似的题)。线段树的概念比较抽象,应用时要注意与实际问题的抽象简化。
线段树是一颗近似的完全二叉树,每个节点代表一个区间,节点的权值是该区间的最小值。根节点是整个区间。每个节点的左孩子是该节点所代表的的区间的左半部分,右孩子是右半部分。查询和更新操作为O(logn)。建树为O(n)。
以{5,9,7,4,6,1}为例
为方便起见,先建立一个node,该node包含闭区间[start,end]及数据data
class Node//节点 { int start;//区间的左端点 int end;//区间的右端点 int data;//该节点的值 public Node(int start,int end)//构造方法中传入左端点和右端点 { this.start = start; this.end = end; } public String toString() { return "["+start+","+end+"]:"+data; } }
建立线段树
//构建线段树并赋予最小值 public static void build(Node[] nodes,int[] base,int index,int start,int end){ Node node=new Node(start,end); nodes[index]=node; if(start==end){ //递归结束 node.data=base[start]; return ; } //未结束,递归构建树 int med=(start+end)/2; build(nodes,base,index*2+1,start,med); build(nodes,base,index*2+2,med+1,end); node.data=Math.min(nodes[index*2+1].data,nodes[index*2+2].data); }
注意,这里应用了递归的一个技巧,当当前节点的值不知道时,因为是递归构建,所以回溯时下面的值其实已经构建完了,这个时候可以继续递归下面的值,通过递归下面的值来构建当前值。
通过线段树查询某个区间的最值(这里是最小值)
思考一下,分情况递归
//区间查询,查询某个区间内的最小值 public static int query(Node[] nodes,int index,int start,int end){ //同样要进行递归查询,三种情况 if(end<nodes[index].start||start>nodes[index].end)return Integer.MAX_VALUE;//当前节点区间不包含待查询区间 if(start<=nodes[index].start&&nodes[index].end<=end){//待查询区间完全包含当前区间,不能再往下查,因为已经是当前最小的了 return nodes[index].data; } return Math.min(query(nodes,index*2+1,start,end),query(nodes,index*2+2,start,end));//部分包含,递归查询 }
线段树更新某个节点的值
//更新单个节点的值,update为更新索引,updateData为更新数据 public static void update(Node[] nodes,int index,int update,int updateData){ if(nodes[index].start==nodes[index].end&&nodes[index].start==update){ //递归结束条件,找到要更新的单个节点 nodes[index].data=updateData; return; } //递归未结束,递归查找 int med=(nodes[index].start+nodes[index].end)/2;//下一层的分界线 if(update<=med)update(nodes,index*2+1,update,updateData); else update(nodes,index*2+2,update,updateData); //更新当前节点 nodes[index].data=Math.min(nodes[index*2+1].data,nodes[index*2+2].data); }
线段树更新某段区间的值
如果是对区间内的数据做某种统一处理(比如某一区间的所有值都加上了5,那么整个线段树都需要变化),那么就要使用延迟更新思想降低复杂度,延迟更新即对区间(node)打上标记,先对节点进行更新并打上标记(此时左右子树没必要打标记,因为查询时判断到当前节点有没有标记,若果有那么按照标记更新左右子树,左右子树打上标记,同时当前节点标记清空,代表当前节点已完成了对左右子树的打标记与实际处理,而左右子树仍未对左右子树的左右子树进行打标记与实际处理,当轮到左右子树时才再一次进行打标记与实际处理)。查询时先要判断当前节点有没有标记,有标记就进行左右子树更新。更新时同理,先判断当前节点有没有标记,再进行下一步递归处理。
延迟更新的概念也比较抽象,这里给一个完整的demo,最好动手实现以下。
节点定义为:
class Node//节点 { int start;//区间的左端点 int end;//区间的右端点 int data;//该节点的值 int mark;//标记 public Node(int start,int end)//构造方法中传入左端点和右端点 { this.start = start; this.end = end; } public String toString() { return "["+start+","+end+"]:"+data; } }
构建树的方法同上
更新区间的方法为:
//更新整个区间,假设区间值要加上updateData public static void updateScope(Node[] nodes,int index,int start,int end,int updateData){ if(end<nodes[index].start||start>nodes[index].end)return ;//当前节点区间不包含待查询区间,不需要更新 if(start<=nodes[index].start&&nodes[index].end<=end){ //待查询区间完全包含当前区间,对于该范围进行实际操作并打上标记,因此需要做的是对左右子树进行更新 nodes[index].data+=updateData;//实际更新 nodes[index].mark=updateData;//打上标记 return ; } //试着更新左右子树,依赖于当前节点是否打了标记 extend(nodes,index); updateScope(nodes,index*2+1,start,end,updateData); updateScope(nodes,index*2+2,start,end,updateData); nodes[index].data=Math.min(nodes[index*2+1].data,nodes[index*2+2].data); } //辅助方法,当前范围的左右子树需要更新 private static void extend(Node[] nodes,int index) { Node node = nodes[index];//获取该下标的节点 if(node.mark!=0) { nodes[index*2+1].mark=node.mark;//更新左子树的标志 nodes[index*2+2].mark=node.mark;//更新右子树的标志 nodes[index*2+1].data += node.mark;//左子树的值加上标志值 nodes[index*2+2].data += node.mark;//右子树的值加上标志值 node.mark=0;//清除当前节点的标志值 } }
查询方法变更,要先判断标记是否存在:
//区间查询,查询某个区间内的最小值 public static int query(Node[] nodes,int index,int start,int end){ //同样要进行递归查询,三种情况 if(end<nodes[index].start||start>nodes[index].end)return Integer.MAX_VALUE;//当前节点区间不包含待查询区间 if(start<=nodes[index].start&&nodes[index].end<=end){//待查询区间完全包含当前区间,不能再往下查,因为已经是当前最小的了 return nodes[index].data; } extend(nodes,index);//因为有了标记的存在,需要在查询时试着更新当前节点的左右子树 return Math.min(query(nodes,index*2+1,start,end),query(nodes,index*2+2,start,end));//部分包含,递归查询 }
如果不是特别需要,对于每个节点执行一次单更新即可,如果要统一处理,显然延迟更新快的多。
demo
public class code { //构建线段树 public static void main(String[] args){ int[] base=new int[]{9,6,4,1,5,2,3,6,5,8}; Node[] nodes=new Node[base.length*2+200]; build(nodes,base,0,0,base.length-1); for(Node node:nodes){ if(node!=null)System.out.println(node.toString()); } //基本查询 System.out.println(query(nodes,0,0,2)); //更新单个节点 update(nodes,0,0,1);//此时base[0]=1 System.out.println(query(nodes,0,0,2)); //更新区间 updateScope(nodes,0,0,9,100); System.out.println(query(nodes,0,0,2)); } //构建线段树并赋予最小值 public static void build(Node[] nodes,int[] base,int index,int start,int end){ Node node=new Node(start,end); nodes[index]=node; if(start==end){ //递归结束 node.data=base[start]; return ; } //未结束,递归构建树 int med=(start+end)/2; build(nodes,base,index*2+1,start,med); build(nodes,base,index*2+2,med+1,end); node.data=Math.min(nodes[index*2+1].data,nodes[index*2+2].data); } //区间查询,查询某个区间内的最小值 public static int query(Node[] nodes,int index,int start,int end){ //同样要进行递归查询,三种情况 if(end<nodes[index].start||start>nodes[index].end)return Integer.MAX_VALUE;//当前节点区间不包含待查询区间 if(start<=nodes[index].start&&nodes[index].end<=end){//待查询区间完全包含当前区间,不能再往下查,因为已经是当前最小的了 return nodes[index].data; } extend(nodes,index);//因为有了标记的存在,需要在查询时试着更新当前节点的左右子树 return Math.min(query(nodes,index*2+1,start,end),query(nodes,index*2+2,start,end));//部分包含,递归查询 } //更新单个节点的值 public static void update(Node[] nodes,int index,int update,int updateData){ if(nodes[index].start==nodes[index].end&&nodes[index].start==update){ //递归结束条件,找到要更新的单个节点 nodes[index].data=updateData; return; } //递归未结束,递归查找 int med=(nodes[index].start+nodes[index].end)/2;//下一层的分界线 if(update<=med)update(nodes,index*2+1,update,updateData); else update(nodes,index*2+2,update,updateData); //更新当前节点 nodes[index].data=Math.min(nodes[index*2+1].data,nodes[index*2+2].data); } //更新整个区间,假设区间值要加上updateData public static void updateScope(Node[] nodes,int index,int start,int end,int updateData){ if(end<nodes[index].start||start>nodes[index].end)return ;//当前节点区间不包含待查询区间,不需要更新 if(start<=nodes[index].start&&nodes[index].end<=end){ //待查询区间完全包含当前区间,对于该范围进行实际操作并打上标记,因此需要做的是对左右子树进行更新 nodes[index].data+=updateData;//实际更新 nodes[index].mark=updateData;//打上标记 return ; } //试着更新左右子树,依赖于当前节点是否打了标记 extend(nodes,index); updateScope(nodes,index*2+1,start,end,updateData); updateScope(nodes,index*2+2,start,end,updateData); nodes[index].data=Math.min(nodes[index*2+1].data,nodes[index*2+2].data); } //辅助方法,当前范围的左右子树需要更新 private static void extend(Node[] nodes,int index) { Node node = nodes[index];//获取该下标的节点 if(node.mark!=0) { nodes[index*2+1].mark=node.mark;//更新左子树的标志 nodes[index*2+2].mark=node.mark;//更新右子树的标志 nodes[index*2+1].data += node.mark;//左子树的值加上标志值 nodes[index*2+2].data += node.mark;//右子树的值加上标志值 node.mark=0;//清除当前节点的标志值 } } } //节点 class Node { int start;//区间的左端点 int end;//区间的右端点 int data;//该节点的值 int mark;//标记 public Node(int start,int end)//构造方法中传入左端点和右端点 { this.start = start; this.end = end; } public String toString() { return "["+start+","+end+"]:"+data; } }