线段树

作者: _PatrickStar | 来源:发表于2021-10-29 15:37 被阅读0次

线段树相关知识点梳理

  • 1.线段树实现:包括add,update,query方法的实现
  • 2.业务代码简单验证
  • =========思路============
  • 我现在面临的问题是什么?
  • 1.有一个需求,需要我将数组中5~99的所有值都统统加5,那么如果按照正常的遍历去实现,时间复杂度是O(N)的。
  • 2.假设这个需求调用十分频繁,可想而知会造成比较大的性能消耗。
  • 所以线段树就是处理上述问题的,尤其明显的优势:如区间类的更新,累加,查询都适用。
  • 先说结论;
  • 线段树可以将时间复杂度降到O(logN)级别,并且需要的额外空间复杂度不会超过4*N(画图知道,堆的结构类似一个小山,从上到下铺满了也就是1,2,4,8,16这样铺下来,我们会发现及时arr长度等于N 比如=8的时候 前面的节点个数=7 我们近似看作8 这就是2N,又下面那排最大不超过2N,所以我们只需要准备一个4N的空间,绝对够用)
  • 我们需要构造一个类似堆的数据;假设我们原数组内一共有4个数,下标从1-4(这里不从0开始是方便思考)
  • 我们需要进行如下转换
  • [3,7,5,9]=>[24,10,14,3,7,5,9] 再看一组数据
  • [3,7,5,9,1] => [25,10,15,3,7,5,10,null,null,null,null,null,null,9,1]
  • 我们姑且把原数组叫arr,转换后的数组叫sum(sum也是从1开始下标)
  • 规律就是,先把后面的数组用堆的形势展开,然后sum[1]的位置记录的是arr[1-4]的累加和sum[2]是arr[1-2]的累加和(左孩子)
  • null那些说明最后一行有些空间没有用到,但是还是分配了。结构的最后还需要写入信息。
  • (当然这种遇到无法被2整除的,是让左孩子分配更多还是右孩子分配更多无所谓,看个人喜好)。比如你把1-5 分成1-3 4-5也可以。
  • 基本结构有了,我们需要在分情况进行处理。
  • 如果我有一个格子 存放的是2-10的累加和,那么如果新任务也是需要在2-10上加,那么就直接在这层加就好了,不需要在把2-10往下拆,继续加。
  • 我们还可以引入一个数组,姑且叫做lazy。它的作用是假设之前任务是在3-29之间都加5 那么我们可以在3-29对应的数组下标同位置的lazy数组下记录一个5,
  • 表示这里有一个5要累加,但是可以先不加,因为我们可以看后续还会收到什么命令。如果后续命令是累加2 那么我们直接在lazy数组那里改成7,
  • 知道出现一个任务不是把3-29全包,比如出现个新任务要求在3-9之间累加6,
  • 那么我们就必须先将之前lazy下来的任务下发,累加完毕,然后在根据3-9往下拆,拆到合适的位置在lazy一个6.
  • 当然有可能3-9其实是由3-6,7-9拼成的。没关系,我们直接将连各个分别lazy之后最后结算的时候累加一下即可
  • */
public class SegmentTree_31 {
    //构造一下SegmentTree类
    public static class SegmentTree {
        /**
         * max 原数组长度
         * arr 维护一个从1开始的原数组
         * sum 累加和数组 维护区间累加和
         * lazy 懒数组,更新慵懒标记
         * change 更新数组 维护更新情况
         * update 布尔值 是否更新
         * */
        private int maxN;
        private int[] arr;
        private int[] sum;
        private int[] lazy;
        private int[] change;
        private boolean[] update;
        public SegmentTree(int[] origin){
            maxN = origin.length+1;
            arr = new int[maxN];
            for (int i=1;i<maxN;i++){
                arr[i] = origin[i-1];
            }
            sum = new int[maxN<<2];
            lazy = new int[maxN<<2];
            change = new int[maxN<<2];
            update = new boolean[maxN<<2];
        }
        private void pushUp(int rt){
            sum[rt] = sum[rt<<1]+sum[rt<<1|1];
        }

        private void pushDown(int ln, int rn, int rt){
            // 更新和累加任务如果都存在的话,肯定是要先更新 然后再累加的。所以先判断updete再去判断lazy
            //下面这段代码就是说如果存在更新,那么我下面这些数组要怎么处理
            if(update[rt]){
                // 左右孩子的更新状态设为true
                update[rt<<1] = true;
                update[rt<<1|1] = true;
                // 左右孩子区间更新的值为change[rt]
                change[rt<<1] = change[rt];
                change[rt<<1|1] = change[rt];
                // 左右孩子区间累加和
                sum[rt<<1] = change[rt]*ln;
                sum[rt<<1|1] = change[rt*rn];
                // 更新的时候,左右孩子区间lazy数组设为0
                lazy[rt<<1] = 0;
                lazy[rt<<1|1] = 0;
                // 一定要记住吧update[rt]设为false
                update[rt] =false;
            }
            // 如果lazy位不是0,说明之前已经懒了一些任务在这里了,这个时候要更新lazy数组和sum数组
            if(lazy[rt]!=0){
                lazy[rt<<1]+=lazy[rt];
                lazy[rt<<1|1]+=lazy[rt];
                sum[rt<<1]+=lazy[rt]*ln;
                sum[rt<<1|1]+=lazy[rt]*rn;
                lazy[rt]=0;
            }
        }
        // 初始化构建sum数组,在arr[l~r]区间 去build
        // l r 代表的是arr的左右区间下标 rt代表的是当前这个sum的下标,这个位置的sum 囊括的就是arr[l~r]的累加和
        // 分情况考虑l=r 和l!=r
        private void build(int l, int r, int rt){
            if (l==r){
                sum[rt] = arr[l];
                return;
            }
            int mid = (l+r)>>1;
            build(l,mid,rt<<1);
            build(mid+1,r,rt<<1|1);
            pushUp(rt);
        }
        //L ,R ,C 参数代表整个累加任务是从L到R区间,每个数加个C
        //l r rt参数代表从最初任务一路往下拆下来,当前的sum节点,rt代表他的下标,l,r分别记录了他所囊括的arr是l-r的区间
        private void add(int L, int R, int C,int l, int r, int rt){
            if(L<=l&&r<=R){
                //说明当前所在的sum节点是完全被任务所包围的,比如我任务是要在1-9区间加个5,现在这个位置是2-7
                sum[rt]+=C*(r-l+1);
                lazy[rt]+=C;
                return;
            }
            int mid = (l+r)>>1;
            //先pushDown,将左右孩子区间的sum lazy数据先更新正确
            pushDown(mid-l+1,r-mid,rt);
            if(L<=mid){
                add(L,R,C,l,mid,rt<<1);
            }
            if (R>mid){
                add(L,R,C,mid+1,r,rt<<1|1);
            }
            pushUp(rt);
        }
        private void update(int L, int R, int C,int l, int r, int rt){
            if(L<=l&&r<=R){
                //说明当前所在的sum节点是完全被任务所包围的,比如我任务是要在1-9区间统一更新成5,现在这个位置是2-7
                update[rt] = true;
                change[rt] = C;
                sum[rt]=C*(r-l+1);
                lazy[rt]=0;
                return;
            }
            int mid = (l+r)>>1;
            //先pushDown,将左右孩子区间的sum lazy数据先更新正确
            pushDown(mid-l+1,r-mid,rt);
            if(L<=mid){
                update(L,R,C,l,mid,rt<<1);
            }
            if (R>mid){
                update(L,R,C,mid+1,r,rt<<1|1);
            }
            pushUp(rt);
        }
        // 因为查询不需要修改数据 所以省去了参数C
        private long query(int L, int R,int l, int r, int rt){
            if(L<=l&&r<=R){
                return sum[rt];
            }
            int mid = (l+r)>>1;
            //先pushDown,将左右孩子区间的sum lazy数据先更新正确
            pushDown(mid-l+1,r-mid,rt);
            long ans = 0;
            if(L<=mid){
                ans+=query(L,R,l,mid,rt<<1);
            }
            if (R>mid){
                ans+=query(L,R,mid+1,r,rt<<1|1);
            }
            return ans;
        }
    }
    // 写一个业务代码用做对数器
    public static class Right{
        private int[] arr;
        public Right(int[] origin){
            arr = new int[origin.length+1];
            for (int i=1;i<arr.length;i++){
                arr[i] = origin[i-1];
            }
        }
        public void add(int L,int R,int C){
            for(int i=L;i<=R;i++){
                arr[i] = arr[i]+C;
            }
        }
        public void update(int L,int R,int C){
            for(int i=L;i<=R;i++){
                arr[i] = C;
            }
        }
        // 查询l-r上的累加和
        public long query(int L,int R){
            long ans = 0;
            for(int i=L;i<=R;i++){
                ans+=arr[i];
            }
            return ans;
        }
    }

    //写个测试
    public static void main(String[] args) {
        int[] origin = new int[]{3,5,6,-5,58,9,5};
        SegmentTree segmentTree = new SegmentTree(origin);
        Right right = new Right(origin);
        segmentTree.build(0, origin.length, 1);
        long num1 = segmentTree.query(2, origin.length-1,0, origin.length, 1);
        long num2 = right.query(2,origin.length-1);
        System.out.println(num1);
        System.out.println(num2);
        System.out.println(num1==num2);
    }
}

相关文章

  • 算法模板(七) 线段树

    线段树单点操作 线段树区间操作

  • 数据结构-线段树

    实现一个线段树 下面实现的线段树,有三个功能: 把数组构建成一颗线段树 线段树的修改 线段树的查询 303号问题 ...

  • 线段树系列之——区间更新

    第三篇线段树了——重点不在于解决题目,通过题目理解线段树才是重点 前面写了一篇关于线段树的单点更新,线段树的单点更...

  • 线段树模板

    线段树 线段树基本概念 概述 线段树,类似区间树,是一个完全二叉树,它在各个节点保存一条线段(数组中的一段子数组)...

  • 线段树专题整理

    待更新 线段树讲解(未读)线段树模板(未读) 模板 求区间总和 A - 敌兵布阵 HDU - 1166 题意 线段...

  • 线段树 02 构建线段树

    构建线段树 线段树的每个节点除了天然的对应一段长度外,不一定赋予其上的意义就是区间元素的和,所以两个节点向上汇聚成...

  • 线段树(区间树)

    线段树:线段树是一种二叉树,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。线段树适用于不变...

  • 线段树

    专题 B线段树求逆序对 C[] D 区间不同值求和

  • 线段树

    [toc] 线段树 实现问题:常用于求数组区间最小值 时间复杂度:(1).建树复杂度:nlogn。(2).线段树算...

  • 线段树

    参考网址:https://www.jianshu.com/p/91f2c503e62f 使用线段树的原因 原来的需...

网友评论

      本文标题:线段树

      本文链接:https://www.haomeiwen.com/subject/yzbialtx.html