线段树

定义

线段树(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();
    }
}

小结

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

posted @ 2019-10-08 11:13  Qiu_Jiaqi  阅读(441)  评论(0编辑  收藏  举报