• 线段树


    定义

    线段树(Segment Tree)是一种二叉搜索树,它将一个区间不断二分,分成两个区间,直到最后只剩一个单元区间即长度为 1 的区间。每个单元区间对应线段树中的一个叶子结点。

    线段树进行更新(update)操作的时间复杂度为O(logn),进行区间查询(range query)操作的时间复杂度也为O(logn)

    实现原理

    结构

    • 线段树是平衡二叉树。(最大深度和最小深度之差不大于 1)
    • 线段树不是完全二叉树,但可以把不存在结点看作 null,即看似是完全二叉树。
    • 线段树是用一个数组保存的。
    • 线段树中只有度为 2 或 0的结点,因为是区间不断二分生成的。

    结构图:

    二叉树的特性

    • 一颗二叉树中,若度为 2 的结点数为 n2,度为 0 的结点数(即叶子结点数)为 n0。则 n0 = n2 + 1。
    • 对 k 层满二叉树:
      • 一共有 2k - 1个结点。
      • 不计最后一层,即前 k - 1 层结点数之和为 2k-1 - 1。
      • 最后一层有 2k-1 个结点。
      • 最后一层结点数比前 k - 1 层结点数还要多 1。

    空间需求

    • 理论空间需求:若叶子结点数为 n,则非叶子结点数为 n - 1,所需空间为 2n - 1。

      如上图中,有 10 个单位区间,即 10 个叶子结点,所以非叶子结点有 9 个,一共需要 19 个空间。的确如此,但是我们发现最后一层中间是空的,我们要把它补上。这就导致实际空间需求并不止这么多。

    • 实际空间需求:4n - 1

      • 最完美的情况就是刚好是满二叉树,叶子结点都在最后一层,若叶子结点数为 n,这时只需 2n - 1 的空间。(通常我们直接开 2n 空间)
      • 但若在此时增加一个叶子结点,将需要开一层的空间(开一整层的原因是,你并不知道这个新结点是在最后一层的哪个位置,如果在最右边的话,需要开一整层的空间),最后一层的空间大小是前面所有层结点数之和再加1。所以空间需求是 2n - 1 + 2n 即 4n - 1。(通常我们直接开 4n 空间)

      这就是为什么需要四倍空间的原因了。

    线段树的构建

    • 线段树的构建是自底向上构建的,从每个叶子结点往上,除了叶子结点,其它结点的值都是根据左右孩子结点的值计算得出的。这个计算过程可能依所需要处理的问题不同而不同(例如对于保存区间最小值的线段树来说,merge的过程应为min()函数,可以求最小值、最大值、总和、最大公约数、最小公倍数等)。
    • 线段树下标从 0 开始,则左孩子结点下标为 2 * index + 1,右孩子结点下标为 2 * index + 2。

    合成器代码:

    package datastructure.tree;
    
    /**
     * 合成器,使线段树可以自定义生成方式
     *
     * @author holiday
     * @version 1.0
     * @date 2019-10-07 16:18
     */
    public interface Merger<E> {
        /**
         * 合成方法,计算出的父结点对应的值
         *
         * @param a 父结点下的左结点
         * @param b 父结点下的右结点
         * @return 根据a和b,计算出的父结点对应的值
         */
        E merge(E a, E b);
    }
    
    

    线段树代码:

    package datastructure.tree;
    
    /**
     * 线段树
     *
     * @author holiday
     * @version 1.0
     * @date 2019-10-07 16:19
     */
    
    public class SegmentTree<E> {
        /**
         * 线段树中的结点,其中父结点的值为它的两个子结点 merge 后的值
         */
        private E[] tree;
    
        /**
         * 生成线段树所用的数组,即各叶子结点
         */
        private E[] data;
        /**
         * 合成器,构造线段树时候同时传入合成器
         */
        private Merger<E> merger;
    
        public SegmentTree(E[] data, Merger<E> merger) {
            this.merger = merger;
            this.data = (E[]) new Object[data.length];
            // 复制原始数据到 data 中
            System.arraycopy(data, 0, this.data, 0, data.length);
            // 开4倍空间
            tree = (E[]) new Object[4 * data.length];
            // 构造线段树
            build(0, 0, data.length - 1);
        }
    
        /**
         * 构建线段树
         *
         * @param treeIndex 当前结点的下标
         * @param treeLeft  当前树的左边界
         * @param treeRight 当前树的右边界
         */
        private void build(int treeIndex, int treeLeft, int treeRight) {
            // 已经到叶子结点
            if (treeLeft == treeRight) {
                tree[treeIndex] = data[treeLeft];
                return;
            }
            // 获得左右孩子下标
            int leftChildIndex = getLeftChildIndex(treeIndex);
            int rightChildIndex = getRightChileIndex(treeIndex);
            // 取中点
            int mid = (treeLeft + treeRight) >>> 1;
            // 先构造左右孩子结点
            build(leftChildIndex, treeLeft, mid);
            build(rightChildIndex, mid + 1, treeRight);
            // 根据左右孩子结点的值,通过合成器决定父结点的值
            tree[treeIndex] = merger.merge(tree[leftChildIndex], tree[rightChildIndex]);
        }
    
        /**
         * 返回左孩子的下标
         *
         * @param index 当前结点的下标
         * @return 左孩子的下标
         */
        private int getLeftChildIndex(int index) {
            return 2 * index + 1;
        }
    
        /**
         * 返回右孩子的下标
         *
         * @param index 当前结点的下标
         * @return 右孩子的下标
         */
        private int getRightChileIndex(int index) {
            return 2 * index + 2;
        }
    
        /**
         * 获得线段树的大小
         *
         * @return size
         */
        public int getSize() {
            return data.length;
        }
    
        /**
         * 获得 data 数组下标为 index 的值。
         *
         * @param index data 数组下标
         * @return data[index]
         */
        public E get(int index) {
            if (index < 0 || index >= data.length) {
                throw new IllegalArgumentException("Index is illegal.");
            }
            return data[index];
        }
    
        /**
         * 打印结果测试
         *
         * @return
         */
        @Override
        public String toString() {
            StringBuilder s = new StringBuilder();
            s.append('[');
            for (int i = 0; i < tree.length; i++) {
                if (tree[i] != null) {
                    s.append(tree[i]);
                } else {
                    s.append("null");
                }
                if (i != tree.length - 1) {
                    s.append(", ");
                }
            }
            s.append(']');
            return s.toString();
        }
    }
    
    

    线段树的查询

    /**
         * 返回区间 [left,right] 的值
         *
         * @param left  查询的左边界
         * @param right 查询的右边界
         * @return result
         */
    public E query(int left, int right) {
        if (left < 0 || right >= data.length || left > right) {
            throw new IllegalArgumentException("Index is illegal");
        }
        return queryRange(0, 0, data.length - 1, left, right);
    }
    
    /**
         * 递归查询
         *
         * @param treeIndex  当前结点的下标
         * @param treeLeft   当前树的左边界
         * @param treeRight  当前树的右边界
         * @param queryLeft  查询的左边界
         * @param queryRight 查询的右边界
         * @return result
         */
    private E queryRange(int treeIndex, int treeLeft, int treeRight, int queryLeft, int queryRight) {
        // 范围正好对应
        if (queryLeft == treeLeft && queryRight == treeRight) {
            return tree[treeIndex];
        }
        // 获得左右孩子下标
        int leftChildIndex = getLeftChildIndex(treeIndex);
        int rightChildIndex = getRightChileIndex(treeIndex);
        // 取中点
        int mid = (treeLeft + treeRight) >>> 1;
        // 若查询的左边界都大于中点,说明区间完全在右子树;若查询的右边界都小于等于中点,说明区间完全在左子树。
        if (queryLeft > mid) {
            return queryRange(rightChildIndex, mid + 1, treeRight, queryLeft, queryRight);
        } else if (queryRight <= mid) {
            return queryRange(leftChildIndex, treeLeft, mid, queryLeft, queryRight);
        }
        // 否则,左右子树都有
        E leftResult = queryRange(leftChildIndex, treeLeft, mid, queryLeft, mid);
        E rightResult = queryRange(rightChildIndex, mid + 1, treeRight, mid + 1, queryRight);
        // 左右区间结果合并
        E result = merger.merge(leftResult, rightResult);
        return result;
    }
    

    练习题目

    传送门:[LeetCode] 303. 区域和检索 - 数组不可变

    线段树的单点更新

    /**
         * 在线段树中修改 data 数组下标为 index 的值为 val
         *
         * @param index data 数组下标
         * @param val   新值
         */
    public void set(int index, E val) {
        if (index < 0 || index >= data.length) {
            throw new IllegalArgumentException("Index is illegal");
        }
        data[index] = val;
        update(0, 0, data.length - 1, index, val);
    }
    
    /**
         * 更新线段树
         *
         * @param treeIndex 当前结点的下标
         * @param treeLeft  当前树的左边界
         * @param treeRight 当前树的左边界
         * @param index     data 数组下标
         * @param val       新值
         */
    private void update(int treeIndex, int treeLeft, int treeRight, int index, E val) {
        // 已经递归到 data 数组中对应的叶子结点值
        if (treeLeft == treeRight) {
            tree[treeIndex] = val;
            return;
        }
        // 获得左右孩子下标
        int leftChildIndex = getLeftChildIndex(treeIndex);
        int rightChildIndex = getRightChileIndex(treeIndex);
        // 取中点
        int mid = (treeLeft + treeRight) >>> 1;
        // 若修改的下标大于中点,说明在右子树,否则在左子树
        if (index > mid) {
            update(rightChildIndex, mid + 1, treeRight, index, val);
        } else {
            update(leftChildIndex, treeLeft, mid, index, val);
        }
        // 根据修改完的左右孩子节点来重新用合成器生成父结点值
        tree[treeIndex] = merger.merge(tree[leftChildIndex], tree[rightChildIndex]);
    }
    

    练习题目

    传送门:[LeetCode] 307. 区域和检索 - 数组可修改

    线段树代码

    package datastructure.tree;
    
    /**
     * 线段树
     *
     * @author holiday
     * @version 1.0
     * @date 2019-10-07 16:19
     */
    
    public class SegmentTree<E> {
        /**
         * 线段树中的结点,其中父结点的值为它的两个子结点 merge 后的值
         */
        private E[] tree;
    
        /**
         * 生成线段树所用的数组,即各叶子结点
         */
        private E[] data;
        /**
         * 合成器,构造线段树时候同时传入合成器
         */
        private Merger<E> merger;
    
        public SegmentTree(E[] data, Merger<E> merger) {
            this.merger = merger;
            this.data = (E[]) new Object[data.length];
            // 复制原始数据到 data 中
            System.arraycopy(data, 0, this.data, 0, data.length);
            // 开4倍空间
            tree = (E[]) new Object[4 * data.length];
            // 构造线段树
            build(0, 0, data.length - 1);
        }
    
        /**
         * 构建线段树
         *
         * @param treeIndex 当前结点的下标
         * @param treeLeft  当前树的左边界
         * @param treeRight 当前树的右边界
         */
        private void build(int treeIndex, int treeLeft, int treeRight) {
            // 已经到叶子结点
            if (treeLeft == treeRight) {
                tree[treeIndex] = data[treeLeft];
                return;
            }
            // 获得左右孩子下标
            int leftChildIndex = getLeftChildIndex(treeIndex);
            int rightChildIndex = getRightChileIndex(treeIndex);
            // 取中点
            int mid = (treeLeft + treeRight) >>> 1;
            // 先构造左右孩子结点
            build(leftChildIndex, treeLeft, mid);
            build(rightChildIndex, mid + 1, treeRight);
            // 根据左右孩子结点的值,通过合成器决定父结点的值
            tree[treeIndex] = merger.merge(tree[leftChildIndex], tree[rightChildIndex]);
        }
    
        /**
         * 返回区间 [left,right] 的值
         *
         * @param left  查询的左边界
         * @param right 查询的右边界
         * @return result
         */
        public E query(int left, int right) {
            if (left < 0 || right >= data.length || left > right) {
                throw new IllegalArgumentException("Index is illegal");
            }
            return queryRange(0, 0, data.length - 1, left, right);
        }
    
        /**
         * 递归查询
         *
         * @param treeIndex  当前结点的下标
         * @param treeLeft   当前树的左边界
         * @param treeRight  当前树的右边界
         * @param queryLeft  查询的左边界
         * @param queryRight 查询的右边界
         * @return result
         */
        private E queryRange(int treeIndex, int treeLeft, int treeRight, int queryLeft, int queryRight) {
            // 范围正好对应
            if (queryLeft == treeLeft && queryRight == treeRight) {
                return tree[treeIndex];
            }
            // 获得左右孩子下标
            int leftChildIndex = getLeftChildIndex(treeIndex);
            int rightChildIndex = getRightChileIndex(treeIndex);
            // 取中点
            int mid = (treeLeft + treeRight) >>> 1;
            // 若查询的左边界都大于中点,说明区间完全在右子树;若查询的右边界都小于等于中点,说明区间完全在左子树。
            if (queryLeft > mid) {
                return queryRange(rightChildIndex, mid + 1, treeRight, queryLeft, queryRight);
            } else if (queryRight <= mid) {
                return queryRange(leftChildIndex, treeLeft, mid, queryLeft, queryRight);
            }
            // 否则,左右子树都有
            E leftResult = queryRange(leftChildIndex, treeLeft, mid, queryLeft, mid);
            E rightResult = queryRange(rightChildIndex, mid + 1, treeRight, mid + 1, queryRight);
            // 左右区间结果合并
            E result = merger.merge(leftResult, rightResult);
            return result;
        }
    
        /**
         * 在线段树中修改 data 数组下标为 index 的值为 val
         *
         * @param index data 数组下标
         * @param val   新值
         */
        public void set(int index, E val) {
            if (index < 0 || index >= data.length) {
                throw new IllegalArgumentException("Index is illegal");
            }
            data[index] = val;
            update(0, 0, data.length - 1, index, val);
        }
    
        /**
         * 更新线段树
         *
         * @param treeIndex 当前结点的下标
         * @param treeLeft  当前树的左边界
         * @param treeRight 当前树的左边界
         * @param index     data 数组下标
         * @param val       新值
         */
        private void update(int treeIndex, int treeLeft, int treeRight, int index, E val) {
            // 已经递归到 data 数组中对应的叶子结点值
            if (treeLeft == treeRight) {
                tree[treeIndex] = val;
                return;
            }
            // 获得左右孩子下标
            int leftChildIndex = getLeftChildIndex(treeIndex);
            int rightChildIndex = getRightChileIndex(treeIndex);
            // 取中点
            int mid = (treeLeft + treeRight) >>> 1;
            // 若修改的下标大于中点,说明在右子树,否则在左子树
            if (index > mid) {
                update(rightChildIndex, mid + 1, treeRight, index, val);
            } else {
                update(leftChildIndex, treeLeft, mid, index, val);
            }
            // 根据修改完的左右孩子节点来重新用合成器生成父结点值
            tree[treeIndex] = merger.merge(tree[leftChildIndex], tree[rightChildIndex]);
        }
    
        /**
         * 返回左孩子的下标
         *
         * @param index 当前结点的下标
         * @return 左孩子的下标
         */
        private int getLeftChildIndex(int index) {
            return 2 * index + 1;
        }
    
        /**
         * 返回右孩子的下标
         *
         * @param index 当前结点的下标
         * @return 右孩子的下标
         */
        private int getRightChileIndex(int index) {
            return 2 * index + 2;
        }
    
        /**
         * 获得线段树的大小
         *
         * @return size
         */
        public int getSize() {
            return data.length;
        }
    
        /**
         * 获得 data 数组下标为 index 的值。
         *
         * @param index data 数组下标
         * @return data[index]
         */
        public E get(int index) {
            if (index < 0 || index >= data.length) {
                throw new IllegalArgumentException("Index is illegal.");
            }
            return data[index];
        }
    
        /**
         * 打印结果测试
         *
         * @return
         */
        @Override
        public String toString() {
            StringBuilder s = new StringBuilder();
            s.append('[');
            for (int i = 0; i < tree.length; i++) {
                if (tree[i] != null) {
                    s.append(tree[i]);
                } else {
                    s.append("null");
                }
                if (i != tree.length - 1) {
                    s.append(", ");
                }
            }
            s.append(']');
            return s.toString();
        }
    }
    

    小结

    区间更新还没有看,等以后做到这类题再看了。


    ┆ 然 ┆   ┆   ┆   ┆ 可 ┆   ┆   ┆ 等 ┆ 暖 ┆
    ┆ 而 ┆ 始 ┆   ┆   ┆ 是 ┆ 将 ┆   ┆ 你 ┆ 一 ┆
    ┆ 你 ┆ 终 ┆ 大 ┆   ┆ 我 ┆ 来 ┆   ┆ 如 ┆ 暖 ┆
    ┆ 没 ┆ 没 ┆ 雁 ┆   ┆ 在 ┆ 也 ┆   ┆ 试 ┆ 这 ┆
    ┆ 有 ┆ 有 ┆ 也 ┆   ┆ 这 ┆ 会 ┆   ┆ 探 ┆ 生 ┆
    ┆ 来 ┆ 来 ┆ 没 ┆   ┆ 里 ┆ 在 ┆   ┆ 般 ┆ 之 ┆
    ┆   ┆   ┆ 有 ┆   ┆   ┆ 这 ┆   ┆ 降 ┆ 凉 ┆
    ┆   ┆   ┆ 来 ┆   ┆   ┆ 里 ┆   ┆ 临 ┆ 薄 ┆
  • 相关阅读:
    谈谈关于PHP连接数据库的两种方法(PDO&Mysqli)
    关于Mui严格模式下的报错解决方案
    CSS常用选择器的认识
    HTML汇总以及CSS的一些开端
    LAMP架构之PHP-FPM 服务器 转
    httpd配置
    notapai++ 使用小技巧
    转载-notepad++ zend-coding使用
    Apache下的配置文件httpd.conf、httpd-vhosts.conf 转
    Apache本机不同端口多站点配置:httpd-vhosts.conf(转载)
  • 原文地址:https://www.cnblogs.com/qiu_jiaqi/p/SegmentTree.html
Copyright © 2020-2023  润新知