zoukankan      html  css  js  c++  java
  • 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("
    ");
                    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));
    }
    
  • 相关阅读:
    jQuery解析XML
    jQuery常用AJAX-API
    jQuery练习
    jQuery常用Event-API
    jQuery常用Method-API
    jQuery九类选择器
    js对象和jQuery对象的区别
    js对象和jQuery对象相互转换
    jQuery入门
    JSON
  • 原文地址:https://www.cnblogs.com/sout-ch233/p/13160391.html
Copyright © 2011-2022 走看看