线段树思想拆解(上篇)

线段树思想拆解(上篇)

思与行,思考过后的实践非常重要,这是将技能融汇贯通的重要一步。

线段树,光从名字上有些莫名奇妙不知所云。

标准线段树是一种可以在 任意范围上进行 querySum 操作 add 操作 update 操作的时间复杂度都达到 O(logN) 的级别。

为达到这种时间复杂度,实际思想采用了空间换时间的方法,对原本数据集进行分段,维护懒更新数组,懒添加数组,对于范围的增加和更新操作阻塞下发,仅在必要时下发阻塞任务(如更新范围小于阻塞范围时),构建这种逻辑概念上的树型结构(实际实现上未必是树结构)

举个例子,比如我有如下一组数

[1,2,3,4,5,6,7,8]

sum 数组对应会组织成的树结构,表示对应范围的数据总和

       							1-8
 				 1-4       							 5-8
     1-2   			3-4   						5-6   		7-8 
   1-1  2-2  	3-3  4-4  					5-5  6-6  		7-7   8-8

最底层是所有的单个数据,父节点是一个范围段,根节点是整个范围 [1,8] 这里为了保持满二叉树会保证 2^n 个节点,那对于元数据集不是 2^n 的情况会进行补充不足的位置补 0 。

我们需要一个结构来存储拓展后的逻辑树型结构,这里采用数组的形式,给定原数组 4 倍的容量,这个容量可能会有空余,为了避免动态计算这里简单选择给定一个冗余空间。在初始化时我们需要构建初始化 sum 数组,这里通过递归的方式进行计算

public class SegmentTree {
    private int orgLength;
    private int helpLength;
    private int[] ogArray;
    private int[] sum;

    public SegmentTree(int[] arr) {
        orgLength = arr.length + 1;
        helpLength = orgLength << 2;
        // 下标从 1 开始
        ogArray = new int[orgLength];
        for (int i = 0; i < arr.length; i++) {
            ogArray[i + 1] = arr[i];
        }
        lazyAdd = new int[helpLength];
        sum = new int[helpLength];
        build(1, 1, orgLength - 1);
    }
		/**
     * @param index 从 index 位置开始构建左右子树的 sum 信息
     * @param l     源数组左范围
     * @param r     源数组右范围
     * @return void
     * @description: 对于 i 位置 二分左孩子是 2*i  i << 1 二分右孩子是  2*i+1 = i << 1 | 1
     */
    public void build(int index, int l, int r) {

        if (l > r) {
            return;
        }
        if (l == r) {
            sum[index] = ogArray[l];
            return;
        }
        int mid = (l + r) >> 1;
        // 递归调用把做孩子和有孩子都构建好
        build(index << 1, l, mid);
        build(index << 1 | 1, mid + 1, r);
        // 构建好后对当前 index 节点进行赋值
        sum[index] = sum[index << 1] + sum[index << 1 | 1];
    }
}

对于范围添加方法,想要实现 O(logN) 的时间复杂度,需要增加存储结构来实现,这我们增加懒添加数组来实现 add 方法

		/**
     * @param addL   addL-addR 任务的范围
     * @param addR
     * @param L      L-R 表达的范围
     * @param R
     * @param addNum 在哪个位置上找表达范围
     * @return void
     * @description: 给 [addL,addR] 范围上的数增加 addNum 个数
     */
    private void add(int addL, int addR, int L, int R, int index, int addNum) {
        // 如果全包则阻塞记录懒信息,更新当前范围位置 sum 值
        if (addL <= L && R <= addR) {
            sum[index] += addNum * (R - L + 1);
            lazyAdd[index] += addNum;
            return;
        }
        // 如果包不住,需要分隔下发
        int mid = (L + R) >> 1;
        // 分发前先将当前的懒任务下发下去
        pushDownLazyTask(index, mid - L + 1, R - mid);
        if (addL <= mid) {
            add(addL, addR, L, mid, index << 1, addNum);
        }
        if (addR > mid) {
            add(addL, addR, mid + 1, R, index << 1 | 1, addNum);
        }
        // 添加后更新当前节点懒信息
        sum[index] = sum[index << 1] + sum[index << 1 | 1];
    }

    private void pushDownLazyTask(int index, int leftRange, int rightRange) {

        if (lazyAdd[index] != 0) {
            int lazyNum = lazyAdd[index];
            lazyAdd[index] = 0;
            lazyAdd[index << 1] += lazyNum;
            lazyAdd[index << 1 | 1] += lazyNum;
            sum[index << 1] += leftRange * lazyNum;
            sum[index << 1 | 1] += rightRange * lazyNum;
        }
    }

这里 add 方法我们可以写一个重载方法,便于简单调用

    public void add(int l, int r, int addNum) {
        add(l, r, 1, orgLength - 1, 1, addNum);
    }

到这里我们已经处理好了初始化以及添加方法,接下来实现范围的 query 方法

    public int query(int queryL, int queryR) {
        return query(queryL, queryR, 1, orgLength - 1, 1);
    }

    /**
     * @param queryL      [queryL,queryR] 要查询的范围
     * @param queryR
     * @param chooseL     [chooseL,chooseR] 当前选择的表达范围(扩充后线段树数组范围)
     * @param chooseR
     * @param chooseIndex 表达范围的 index
     * @return long
     * @description:
     */
    private int query(int queryL, int queryR, int chooseL, int chooseR, int chooseIndex) {

        if (queryL <= chooseL && queryR >= chooseR) {
            return sum[chooseIndex];
        }

        //  1,2,3,4,5,6,7,8
        // 1+7 >> 1 = 4  为啥 queryL 是小于呢,因为左半边是 1-4 右半边是 5-8 右半边得 mid+1
        int mid = (chooseL + chooseR) >> 1;
        // 当前节点的数据不全包命中,则需要尝试将懒任务下发,不下发则会导致最终数据不准
        // 4-1 +1 = 4  8-4 = 4
        pushDownLazyTask(chooseIndex, mid - chooseL + 1, chooseR - mid);
        int ans = 0;
        if (queryL <= mid) {
            ans += query(queryL, queryR, chooseL, mid, chooseIndex << 1);
        }
        if (queryR > mid) {
            ans += query(queryL, queryR, mid + 1, chooseR, chooseIndex << 1 | 1);
        }
        return ans;
    }

到此为止通过借助 sum 数组,lazy 数组就已经实现了在范围内统一增加一个数值,查询范围总和的时间复杂度为 O(logN) 了,接下来要引入更新操作,对于更新操作我们需要再借助两个数组来维护更新的懒操作。

你可能感兴趣的:(算法,dying搁浅,线段树,区间修改树,数组)