java数据结构-线段树

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_40918961/article/details/96438282

        线段树是一种树形的高级数据结构。用于解决数组的某段区间的求最值,某段区间更新单个/所有值等问题(或抽象简化后与之相似的题)。线段树的概念比较抽象,应用时要注意与实际问题的抽象简化。

        线段树是一颗近似的完全二叉树,每个节点代表一个区间,节点的权值是该区间的最小值。根节点是整个区间。每个节点的左孩子是该节点所代表的的区间的左半部分,右孩子是右半部分。查询和更新操作为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;
    }
}

参考:https://zhuanlan.zhihu.com/p/34150142

猜你喜欢

转载自blog.csdn.net/qq_40918961/article/details/96438282