算法:线段树

概念:

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。 
对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。

图解:

对于一维数组:numberArray = [10, 8, 9, 7, 6, 1, 4, 2, 3]
那么它构成的线段树就是下面这个样子:

线段树的根节点 0 [0, 8] 表示,在区间 [0, 8] 这些数据的总和为 50
1 [0, 4] 表示,在区间 [0, 4] 这些数据的总和为 40
2 [5, 8] 表示,在区间 [5, 8] 这些数据的总和为 10
......
这里的区间是以一维数组的下标作为左右端点的

那么构建这颗线段树有什么好处呢?

查询区间和:

当我们要求一维数组中,[3, 5] 这个区间的值,我们可以快速的通过遍历这棵线段树得到,而且时间复杂度在 O(log(n)) 级别

那么我们如何得到呢?

1.首先从根节点开始遍历,因为 [3, 5] 处于 0 [0, 8] 区间,接着查询 0 号根节点的左右子树
2.查询 1 [0, 4],因为 [0, 4] 包含 [3, 5] 区间的一部分,接着查询 1 号节点的左右子树
3.查询 3 [0, 2],因为 [0, 2] 不在 [3, 5] 区间内部,所以舍弃,返回0,回到第 2 步查询 1 号节点的右子树
4.查询 4 [3, 4],因为 [3, 4] 在 [3, 5] 区间内部,所以返回 4 号节点的值 13,1 号节点的左右子树访问完毕,返回左右子树值的和,也就是返回 0 + 13 = 13,此时 0号根节点的左子树访问完毕,然后回到第 1 步,访问 0 号根节点的右子树
5.查询 2 [5, 8],因为 [5, 8] 包含 [3, 5] 区间的一部分,接着查询 2 号节点的左右子树
6.查询 5 [5, 6],因为 [5, 6] 包含 [3, 5] 区间的一部分,接着查询 5 号节点的左右子树
7.查询 11 [5, 5],因为 [5, 5] 在 [3, 5] 区间内部,返回 11 号节点的值 1,5 号节点的左子树访问完毕,接着访问 5 号节点的右子树
8.查询 12 [6, 6],因为 [6, 6] 不在 [3, 5] 区间内部,所以舍弃,返回0,回到第  6 步,此时 5 号节点的左右子树均访问完毕,返回左右子树值的和,也就是返回 1 + 0 = 1,此时 5 号节点左右子树访问完毕,回到第 5 步,该访问 2 号节点的右子树
9.查询 6 [7, 8],因为 [7, 8] 不在 [3, 5] 区间内部,所以舍弃,返回0,回到第 5 步,此时 2 号节点的左右子树访问完毕,返回左右子树值的和,也就是返回 1 + 0 = 1,此时 2 号节点左右子树访问完毕,回到第 1 步,此时发现根节点的左右子树也访问完毕,所以返回左右子树值的和,也就是返回 13 + 1 = 14

最终得到 [3, 5] 区间和为 14
上面有点啰嗦,看下面的图,也就是找到这些蓝色节点,将蓝色节点的值相加在一起就得到了 [3, 5] 区间的和

创建线段树:

创建线段树的方式很简单,就是递归的创建,如果你学过二叉查找树,那么这个树的建立方式也是和它大同小异的,唯一的区别是,为当前节点创建左右子树,需要左右孩子的值相加之和赋值给当前节点,具体看下面的代码就好了

更新节点:

比如我们想把一维数组中下标为 3 的元素从 7 改为 5,那我们同样要对整颗线段树进行更新
更新的方式和创建的方式差不多,只不过根据更改的下标改对应的左子树或者右子树
就像上面说的那个,把 7 改成 5,我们只需要改动的节点为下图标红的节点值

重点问题:为什么创建的线段树所占空间要是原始数据数组的空间大小的 4 倍呢?

这个线段树就是平衡二叉树,但是同样具备完全二叉树的特点,但是不是真正的完全二叉树
完全二叉树的总节点个数为 2n + 2,n 表示的是原始数据数组的空间大小
我上面画的只能说是凑巧是一颗完全二叉树

推导一下哦:
如果原始数据数组大小为 n
假设:n = 2 ^ k,此时构成的树就是满二叉树,所占空间大小就是 2n
那么如果:n > 2 ^ k,那就意味着需要多一层来存储最后的叶子节点
那么就相当于 n = 2 ^ (k +1)
那么所占空间大小就应该是 4n,实际上用不到 4n,但是为了方便就设置为 4n,3n 是绝对不ok的,不信,你试试 ^_^

既然说到这里,我们回顾一下之前的树节点总个数是怎么算的?
假设树的高度为 k,那么对于满二叉树来说,每层节点都是满的
那么第一层 1, 第二层 2,第三层 4,第 k 层 2^(k-1)
正好为等比数列,根据等比数列的求和公式:

 

a1 = 1,,q =  2,n = k,所以 Sn = 2^k - 1

线段树代码:博主使用的 Java 语言

public class SegmentTree {
    /**
     * nodeArray节点数组
     */
    int[] nodeArray;
    /**
     * numberArray 一维数据数组
     */
    int[] numberArray;

    /**
     * 构造线段树
     *
     * @param numberArray 一维数据数组
     */
    SegmentTree(int[] numberArray) {
        this.numberArray = numberArray;
        // 线段树的节点数组一般要开辟 4 倍大小空间,防止数组越界
        this.nodeArray = new int[this.numberArray.length * 4];
    }

    /**
     * 构建线段树,节点 position [left, right, number]
     *
     * @param position 当前节点位置(节点编号,左节点编号 position * 2 + 1,右节点编号 position * 2 + 2),编号代表在节点数组的下标位置
     * @param left     节点表示区间的左端点
     * @param right    节点表示区间的右端点
     */
    void buildSegmentTree(int position, int left, int right) {
        // 如果区间的左端点 = 右端点,说明此节点为线段树的叶子节点
        if (left == right) {
            nodeArray[position] = numberArray[left];
            return;
        }
        // 区间取半,得到区间中点
        int mid = (left + right) / 2;
        // 建立线段树的左子树
        buildSegmentTree(position * 2 + 1, left, mid);
        // 建立线段树的右子树
        buildSegmentTree(position * 2 + 2, mid + 1, right);
        // 当前节点值 = 左孩子的值 + 右孩子的值
        nodeArray[position] = nodeArray[position * 2 + 1] + nodeArray[position * 2 + 2];
    }

    /**
     * 更新线段树
     *
     * @param position 当前节点位置,所在节点数组的下标位置
     * @param left     节点表示区间的左端点
     * @param right    节点表示区间的右端点
     * @param index    要更改的数据在数据数组中的下标位置
     * @param number   更改的数据
     */
    void updateSegmentTree(int position, int left, int right, int index, int number) {
        // 如果此时为线段树的叶子节点,且叶子节点的左右端点 = 要更改的数据在数据数组中的下标位置,进行重新赋值
        if (left == right && left == index) {
            nodeArray[position] = number;
            return;
        }
        // 区间取半,得到区间中点
        int mid = (left + right) / 2;
        // 如果要更改的数据在数据数组中的下标位置在区间中点的左边,则更新左子树
        if (index <= mid) {
            updateSegmentTree(position * 2 + 1, left, mid, index, number);
        } else {
            // 如果要更改的数据在数据数组中的下标位置在区间中点的右边,则更新右子树
            updateSegmentTree(position * 2 + 2, mid + 1, right, index, number);
        }
        // 当前节点值 = 左孩子的值 + 右孩子的值
        nodeArray[position] = nodeArray[position * 2 + 1] + nodeArray[position * 2 + 2];
    }

    /**
     * 给定区间,查询该区间的和
     *
     * @param position   当前节点位置,所在节点数组的下标位置
     * @param left       节点表示区间的左端点
     * @param right      节点表示区间的右端点
     * @param rangeLeft  给定区间的左端点
     * @param rangeRight 给定区间的右端点
     * @return 该区间的和
     */
    int sumRangeSegmentTree(int position, int left, int right, int rangeLeft, int rangeRight) {
        // 如果节点表示区间不在给定区间内部,则返回 0
        if (rangeLeft > right || rangeRight < left) {
            return 0;
        }
        // 如果节点表示区间在给定区间内部,则返回节点值
        if (rangeLeft <= left && rangeRight >= right) {
            return nodeArray[position];
        }
        // 区间取半,得到区间中点
        int mid = (left + right) / 2;
        // 返回左子树(其中节点在给定区间的值) + 右子树(其中节点在给定区间的值)
        return sumRangeSegmentTree(position * 2 + 1, left, mid, rangeLeft, rangeRight)
                + sumRangeSegmentTree(position * 2 + 2, mid + 1, right, rangeLeft, rangeRight);
    }

}

测试:

public static void main(String[] args) {
        int[] numberArray = new int[]{10, 8, 9, 7, 6, 1, 4, 2, 3};
        SegmentTree segmentTree = new SegmentTree(numberArray);
        segmentTree.buildSegmentTree(0, 0, numberArray.length - 1);
        int result = segmentTree.sumRangeSegmentTree(0, 0, numberArray.length - 1, 3, 5);
        System.out.println("[3, 5] = " + result);
        segmentTree.updateSegmentTree(0, 0, numberArray.length - 1, 3, 5);
        result = segmentTree.sumRangeSegmentTree(0, 0, numberArray.length - 1, 3, 5);
        System.out.println("numberArray[3] = 5,再次查询[3, 5] = " + result);
    }

总结:

如果读者发现博客中有什么错误,可以再下方评论,我看到后会进行改正!
如果有什么不懂的地方,也可以评论,我们互相探讨,共同进步!
最后说一句:好好学习,天天向上!^_^ 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值