10-线段树 Segment Tree

学习资源:慕课网liyubobobo老师的《玩儿转数据结构》


1、简介

  • 线段树是一种二叉搜索树,它将一个大的区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
  • 对于线段树中的每一个非叶子结点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子结点数目为N,即整个线段区间的长度。
  • 使用线段树可以快速的查找某一个结点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,因此有时需要离散化让空间压缩。
  • 线段树不一定是满的二叉树;线段树不是完全二叉树;线段树是平衡二叉树
  • 线段树的使用场景一般是查询,所以线段树所作用的区间本身是固定的。

image-20200613231721237

image-20200614102657378

2、线段树的实现

2.1、线段树的底层实现

线段树是平衡二叉树,内部可以使用数组表示。

我们可以将线段树作为一棵满的二叉树,不存在的结点看作是空即可。

一个长度为n的数据集合,在线段树中总可以使用一个长度为4n的数组容纳。

image-20200614125550209

image-20200614132521710 image-20200614132610153 image-20200614132714708

2.2、融合器Merger

​ 线段树中的结点部分是单个的元素,部分是null结点,剩余的都是一个个的融合后的结点,那么如何表示这样的结点呢?应该视具体的业务而定,这里创建一个融合器接口,传入两个结点,融合为一个结点。

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

2.3、基础部分代码

public class SegmentTree<E> {

    private E[] data;
    private E[] tree;
    private Merger<E> merger;
    
    
    public E get(int index){

        if(index<0 || index>=data.length){
            throw new IllegalArgumentException("索引不合法");
        }
        return data[index];
    }

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

    // 完全二叉树中,当前结点的左孩子结点所在的索引
    private int leftChild(int index){
        return 2*index + 1;
    }
	// 完全二叉树中,当前结点的左孩子结点所在的索引
    private int rightChild(int index){
        return 2*index + 2;
    }
    
    @Override
    public String toString(){

        StringBuilder res = new StringBuilder();
        res.append('[');

        double tier = 1.0;

        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(", ");

            if(i == Math.pow(2.0, tier)-2){

                res.append("\n");
                tier++;
            }
        }
        res.append(']');
        return res.toString();
    }
}

2.4、创建线段树

// 构造器。参数1:传入的数据集合;参数2:融合器
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];
    
    // 参数1:当前创建的线段树的根结点的索引;参数2:当前结点所代表的线段,根结点就是0~最后一个
    buildSegmentTree(0, 0, data.length - 1);
}
// 递归创建
private void buildSegmentTree(int treeIndex, int l, int r) {
    
    // 递归到底,叶子结点,直接return
    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]);
}

2.5、区间查询

seg

// 返回区间[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.");
    
    // 参数1:当前查询的线段树的根节点;参数2:当前结点所代表的线段,根结点就是0~最后一个
    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;
    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);
}

2.6、更新元素

在线段树中更新某个元素,不只需要更新线段树(完全二叉树)中的单个的结点,还要向上追溯更新其父辈结点。

public void set(int index, E e){
    
    if(index<0 || index>=data.length){
        throw new IllegalArgumentException("索引不合法");
    }
    // 更新私有的data数组
    data[index] = e;
    // 再更新tree数组
    set(0, 0, data.length-1, 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;
    int leftTreeIndex = leftChild(treeIndex);
    int rightTreeIndex = rightChild(treeIndex);
    
    // 待更新结点在中点的右侧
    if(index >= mid+1){
        set(rightTreeIndex, mid+1, r, index, e);
    }
    // 待更新结点在中点的左侧
    else {
        set(leftTreeIndex, l, mid, index, e);
    }
    tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
}

2.7、其他操作

  • 更新区间
  • 二维线段树
  • 动态线段树

3、测试

具体测试的时候,需要传入出一个融合器。(可以使用Lambda表达式)

 @Test
public void test(){
    Integer[] nums = {-2,0,3,-5,2,-1};
    SegmentTree<Integer> segmentTree = new SegmentTree<Integer>(nums, new Merger<Integer>() {
        @Override
        public Integer merge(Integer a, Integer b) {
            return a + b;
        }
    });
    System.out.println(segmentTree);
    System.out.println(segmentTree.getSize());
    System.out.println(segmentTree.get(3));
    System.out.println(segmentTree.query(2, 5));
}
posted @ 2020-06-18 22:04  卡文迪雨  阅读(174)  评论(0编辑  收藏  举报