欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

线段树(segment tree)详解

程序员文章站 2022-07-13 11:18:32
...

线段树(segment tree)详解

什么是线段树

线段树是一棵平衡搜索树,但是不是完全二叉树,其实也是一棵二分搜索树,它储存的是一个区间的信息。

每个节点以结构体的方式存储,结构体包含以下几个信息:

  1. 区间左端点、右端点;(这两者必有)

  2. 区间内要维护的信息(实际情况而定,数目不等)。

    一个具体的线段树如下所示,这里是一数组作为底层数据结构来阐述线段树,所以就简单的将线段树看为满二叉树,这样的话肯定是会造成空间的浪费,但是这里就是为了理解线段树这种数据结构,不去考虑其更高层的实现,要想空间不浪费的话就动态的创建线段树就可以了。

线段树(segment tree)详解

构建二叉树

其实只要涉及到树的一些操作就避免不了使用递归的思想来创建树,以为每一棵树都是有一棵棵子树构成的.

主体的思路就是:

  1. 对于二分到的每一个结点,给它的左右端点确定范围。
  2. 如果是叶子节点,存储要维护的信息。
  3. 子树合并。
public class SegmentTree<E> {
    private E[] data;
    private E[] tree;
    private Merger<E> merger;

    public SegmentTree(E[] arr, Merger<E> merger){
        this.merger = merger;
        data =  (E[]) new Object[arr.length];
        for (int i = 0; i < arr.length; i++){
            data[i] = arr[i];
        }
        tree = (E[])new Object[arr.length * 4 ];       //为保证树是一个满二叉树,包含n个节点最差的情况需要的空间是4n的,所以其中肯定有为空的节点
                                                        //其实线段是一颗平衡二叉树,它是一棵空树或它的左右两个子树的高度差的绝对值不超过1
        buildSegmentTree(0,0,arr.length-1);          //创建线段树
    }

    /**
     * 创建表示data区间[l...r]的线段树
     * @param treeIndex 以treeIndex为起点
     * @param L L
     * @param R R
     */
    public void buildSegmentTree(int treeIndex, int L, int R){
        if ( L == R){
            tree[treeIndex] = data[L];
            return;
        }

        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);

        int mid = L + (R - L)/2;

        buildSegmentTree(leftTreeIndex, L, mid);
        buildSegmentTree(rightTreeIndex, mid + 1, R);

        tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);

    }

    **
     * 返回左孩子节点的索引值
     * @param index
     * @return
     */
    public int leftChild(int index){
        return 2 * index + 1;
    }

    /**
     * 返回右孩子节点的索引值
     * @param index
     * @return
     */
    public int rightChild(int index){
        return 2 * index + 2;
    }

上面初始化树的空间的时候使用的空间大小是要存储的数组的大小的4倍,具体是怎么来的?

对于一棵满二叉树,其第h层的元素有2^h-1个节点,并且第h层元素的节点的个数等于前1—>h-1

层元素个数的总和。那么来看存储n个元素的时候需要多大的空间?需要4n的空间,具体如下图:

线段树(segment tree)详解

这里实现了Merger的接口,这个结接口就是用来应对对线段树的不同操作而设置的,用户可以根据自己想要实现的功能自己进行这个接口的具体实现。

public interface Merger<E> {
    E merge(E a, E b);
}

树的查询操作

查询一个线段树其实使用递归的思想来实现,例如说我们要查询[2,5]这个区间内的元素,然后从根节点开始递归,[2,5]是在[0,7]之内的,然后求出根的左右孩子和中间分割点mid,查询区间的左右边界与mid比较,然后根据其满足的条件看需要在左子树还是右子树继续查询,不断递归直到找到要查询的区间。说的有点啰嗦了,具体看下面:

线段树(segment tree)详解

/**
     * 返回区间[queryL, queryR]的值
     * @param queryL queryL
     * @param queryR queryR
     * @return
     */
    public E query(int queryL, int queryR){
        return query(0,0,data.length-1, queryL,queryR);
    }

    /**
     *  以treeIndex为根的线段树中[l...r]的范围里,搜索区间[queryL...queryR]的值
     * @param index
     * @param L
     * @param R
     * @param queryL
     * @param queryR
     * @return
     */
    public E query(int index, int L, int R, int queryL, int queryR){
        if (queryL < 0 || queryR < 0|| queryL > queryR || queryL > data.length || queryR > data.length){
            throw new IllegalArgumentException("Index is illegal");
        }

        if (L == queryL && R == queryR){
            return tree[index];
        }

        int mid = L + (R - L)/2;
        int leftChildIndex = leftChild(index);
        int rightChildIndex = rightChild(index);

        if (queryL > mid){
            return query(rightChildIndex, mid +1, R, queryL,queryR);
        }else if (queryR <= mid){
            return query(leftChildIndex, L, mid, queryL, queryR);
        }

        E leftResult = query(leftChildIndex,L,mid, queryL, mid);
        E rightResult = query(rightChildIndex, mid+1, R, mid +1, queryR);
        return merger.merge(leftResult,rightResult);
    }

树中元素的更新

更新操作其实与查询操作的实现差不多,但是有一点区别的是,更新某个节点的匀速之后其父节点的元素也需要更新,因为其父节点也是包含该元素的,因此更新操作需要返回更新后状态。

具体实现如下:

/**
     * 把索引为index位置的值更新为e
     * @param index index
     * @param e e
     */
    public void set(int index, E e){
        data[index] = e;
        set(0, 0,data.length-1, index, e);
    }

    /**
     * 更新以treeIndex为根节点的[L...R]区间内的index位置的元素的值
     * @param treeIndex
     * @param L
     * @param R
     * @param index
     * @param e
     */
    public void set(int treeIndex, int L, int R, int index, E e){
        if (index < 0 || index > R || index < L ||index > data.length-1){
            throw new IllegalArgumentException("Index is illegal");
        }
        if (L == R){
            tree[treeIndex] = e;
        }

        int mid = L + (R - L)/2;
        int leftTreeChild = leftChild(treeIndex);
        int rightTreeChild = rightChild(treeIndex);
        if (index <= mid){
            set(leftTreeChild, L, mid, index, e);
        }else if (index > mid ){
            set(rightTreeChild, mid +1, R, index, e);
        }
        tree[treeIndex] = merger.merge(tree[leftTreeChild], tree[rightTreeChild]);

    }

完整代码

package SegmentTree;

/**
 * 以数组作为底层的线段树的实现
 * @author WilsonSong
 * @date 2018/6/12
 */
public class SegmentTree<E> {
    private E[] data;
    private E[] tree;
    private Merger<E> merger;

    public SegmentTree(E[] arr, Merger<E> merger){
        this.merger = merger;
        data =  (E[]) new Object[arr.length];
        for (int i = 0; i < arr.length; i++){
            data[i] = arr[i];
        }
        tree = (E[])new Object[arr.length * 4 ];       //为保证树是一个满二叉树,包含n个节点最差的情况需要的空间是4n的,所以其中肯定有为空的节点
                                                        //其实线段是一颗平衡二叉树,它是一棵空树或它的左右两个子树的高度差的绝对值不超过1
        buildSegmentTree(0,0,arr.length-1);          //创建线段树
    }

    /**
     * 创建表示data区间[l...r]的线段树
     * @param treeIndex 以treeIndex为起点
     * @param L L
     * @param R R
     */
    public void buildSegmentTree(int treeIndex, int L, int R){
        if ( L == R){
            tree[treeIndex] = data[L];
            return;
        }

        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);

        int mid = L + (R - L)/2;

        buildSegmentTree(leftTreeIndex, L, mid);
        buildSegmentTree(rightTreeIndex, mid + 1, R);

        tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);

    }

    /**
     * 获取线段树的大小
     * @return
     */
    public int getSize(){
        return data.length;
    }

    /**
     * 获取元素
     * @param index 元素索引
     * @return
     */
    public E get(int index){
        if (index < 0 || index > data.length){
            throw new IllegalArgumentException("index is illegal");
        }
        return data[index];
    }

    /**
     * 返回左孩子节点的索引值
     * @param index
     * @return
     */
    public int leftChild(int index){
        return 2 * index + 1;
    }

    /**
     * 返回右孩子节点的索引值
     * @param index
     * @return
     */
    public int rightChild(int index){
        return 2 * index + 2;
    }

    /**
     * 返回区间[queryL, queryR]的值
     * @param queryL queryL
     * @param queryR queryR
     * @return
     */
    public E query(int queryL, int queryR){
        return query(0,0,data.length-1, queryL,queryR);
    }

    /**
     *  以treeIndex为根的线段树中[l...r]的范围里,搜索区间[queryL...queryR]的值
     * @param index
     * @param L
     * @param R
     * @param queryL
     * @param queryR
     * @return
     */
    public E query(int index, int L, int R, int queryL, int queryR){
        if (queryL < 0 || queryR < 0|| queryL > queryR || queryL > data.length || queryR > data.length){
            throw new IllegalArgumentException("Index is illegal");
        }

        if (L == queryL && R == queryR){
            return tree[index];
        }

        int mid = L + (R - L)/2;
        int leftChildIndex = leftChild(index);
        int rightChildIndex = rightChild(index);

        if (queryL > mid){
            return query(rightChildIndex, mid +1, R, queryL,queryR);
        }else if (queryR <= mid){
            return query(leftChildIndex, L, mid, queryL, queryR);
        }

        E leftResult = query(leftChildIndex,L,mid, queryL, mid);
        E rightResult = query(rightChildIndex, mid+1, R, mid +1, queryR);
        return merger.merge(leftResult,rightResult);
    }

    /**
     * 把索引为index位置的值更新为e
     * @param index index
     * @param e e
     */
    public void set(int index, E e){
        data[index] = e;
        set(0, 0,data.length-1, index, e);
    }

    /**
     * 更新以treeIndex为根节点的[L...R]区间内的index位置的元素的值
     * @param treeIndex
     * @param L
     * @param R
     * @param index
     * @param e
     */
    public void set(int treeIndex, int L, int R, int index, E e){
        if (index < 0 || index > R || index < L ||index > data.length-1){
            throw new IllegalArgumentException("Index is illegal");
        }
        if (L == R){
            tree[treeIndex] = e;
        }

        int mid = L + (R - L)/2;
        int leftTreeChild = leftChild(treeIndex);
        int rightTreeChild = rightChild(treeIndex);
        if (index <= mid){
            set(leftTreeChild, L, mid, index, e);
        }else if (index > mid ){
            set(rightTreeChild, mid +1, R, index, e);
        }
       tree[treeIndex] = merger.merge(tree[leftTreeChild], tree[rightTreeChild]);

    }
}

应用

在LeetCode上有这么一道题:

Given an integer array nums, find the sum of the elements between indices i and j (ij), inclusive.

The update(i, val) function modifies nums by updating the element at index i to val.

Example:

Given nums = [1, 3, 5]

sumRange(0, 2) -> 9
update(1, 2)
sumRange(0, 2) -> 8

Note:

  1. The array is only modifiable by the update function.
  2. You may assume the number of calls to update and sumRange function is distributed evenly.

这道题目其实就是动态查询某个数组区间内的和,一般的想法是进行预处理,就是把前i个元素的和求出来,然后每次求的时候就可以借助这个和来进行sumRange的操作,然后更新操作的话就是初始化一个新的数组存放原来nums的值,然后对其进行跟新,这样更新的话同时sum的和也需要更新,就是从更新的那个元素开始后面的元素的和全部要更新。事件复杂度nO(n)的,超时了。

然后这里要是使用线段树来做的话实现复杂度就是nO(logn)的,能够运行通过,具体如下:

class NumArray {

    private interface Merger<E> {
        E merge(E a, E b);
    }

    private class SegmentTree<E> {

        private E[] tree;
        private E[] data;
        private Merger<E> merger;

        public SegmentTree(E[] arr, Merger<E> merger){

            this.merger = merger;

            data = (E[])new Object[arr.length];
            for(int i = 0 ; i < arr.length ; i ++)
                data[i] = arr[i];

            tree = (E[])new Object[4 * arr.length];
            buildSegmentTree(0, 0, arr.length - 1);
        }

        // 在treeIndex的位置创建表示区间[l...r]的线段树
        private void buildSegmentTree(int treeIndex, int l, int r){

            if(l == r){
                tree[treeIndex] = data[l];
                return;
            }

            int leftTreeIndex = leftChild(treeIndex);
            int rightTreeIndex = rightChild(treeIndex);

            // int mid = (l + r) / 2;
            int mid = l + (r - l) / 2;
            buildSegmentTree(leftTreeIndex, l, mid);
            buildSegmentTree(rightTreeIndex, mid + 1, r);

            tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
        }

        public int getSize(){
            return data.length;
        }

        public E get(int index){
            if(index < 0 || index >= data.length)
                throw new IllegalArgumentException("Index is illegal.");
            return data[index];
        }

        // 返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
        private int leftChild(int index){
            return 2*index + 1;
        }

        // 返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
        private int rightChild(int index){
            return 2*index + 2;
        }

        // 返回区间[queryL, queryR]的值
        public E query(int queryL, int queryR){

            if(queryL < 0 || queryL >= data.length ||
                    queryR < 0 || queryR >= data.length || queryL > queryR)
                throw new IllegalArgumentException("Index is illegal.");

            return query(0, 0, data.length - 1, queryL, queryR);
        }

        // 在以treeIndex为根的线段树中[l...r]的范围里,搜索区间[queryL...queryR]的值
        private E query(int treeIndex, int l, int r, int queryL, int queryR){

            if(l == queryL && r == queryR)
                return tree[treeIndex];

            int mid = l + (r - l) / 2;
            // treeIndex的节点分为[l...mid]和[mid+1...r]两部分

            int leftTreeIndex = leftChild(treeIndex);
            int rightTreeIndex = rightChild(treeIndex);
            if(queryL >= mid + 1)
                return query(rightTreeIndex, mid + 1, r, queryL, queryR);
            else if(queryR <= mid)
                return query(leftTreeIndex, l, mid, queryL, queryR);

            E leftResult = query(leftTreeIndex, l, mid, queryL, mid);
            E rightResult = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);
            return merger.merge(leftResult, rightResult);
        }

        // 将index位置的值,更新为e
        public void set(int index, E e){

            if(index < 0 || index >= data.length)
                throw new IllegalArgumentException("Index is illegal");

            data[index] = e;
            set(0, 0, data.length - 1, index, e);
        }

        // 在以treeIndex为根的线段树中更新index的值为e
        private void set(int treeIndex, int l, int r, int index, E e){

            if(l == r){
                tree[treeIndex] = e;
                return;
            }

            int mid = l + (r - l) / 2;
            // treeIndex的节点分为[l...mid]和[mid+1...r]两部分

            int leftTreeIndex = leftChild(treeIndex);
            int rightTreeIndex = rightChild(treeIndex);
            if(index >= mid + 1)
                set(rightTreeIndex, mid + 1, r, index, e);
            else // index <= mid
                set(leftTreeIndex, l, mid, index, e);

            tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
        }

        @Override
        public String toString(){
            StringBuilder res = new StringBuilder();
            res.append('[');
            for(int i = 0 ; i < tree.length ; i ++){
                if(tree[i] != null)
                    res.append(tree[i]);
                else
                    res.append("null");

                if(i != tree.length - 1)
                    res.append(", ");
            }
            res.append(']');
            return res.toString();
        }
    }

    private SegmentTree<Integer> segTree;

    public NumArray(int[] nums) {

        if(nums.length != 0){
            Integer[] data = new Integer[nums.length];
            for(int i = 0 ; i < nums.length ; i ++)
                data[i] = nums[i];
            segTree = new SegmentTree<>(data, (a, b) -> a + b);
        }
    }

    public void update(int i, int val) {
        if(segTree == null)
            throw new IllegalArgumentException("Error");
        segTree.set(i, val);
    }

    public int sumRange(int i, int j) {
        if(segTree == null)
            throw new IllegalArgumentException("Error");
        return segTree.query(i, j);
    }
}


/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray obj = new NumArray(nums);
 * obj.update(i,val);
 * int param_2 = obj.sumRange(i,j);
 */