数据结构之线段树(SegmentTree)

线段树是一种高级的数据结构,一般我们根本接触不到他,然而他的出现解决了特定的问题。这种数据结构的设计还是值得我们学习的。

目录

在这里插入图片描述

分析与拓展

1、线段树的设计栗子

假如给定一组数组元素:
A[0],A[1],A[2],A[3],A[4],A[5],A[6],A[7]
这些数组用区间表示可为A[0…7],则我们可以吧[0,7]这个区间不断均分取整,最终得到了如下图的形式。这就是我们的线段树模型。

在这里插入图片描述

1、如上图,每一个节点存储的为一个线段,或者可以说为一个区间的数据。
2、线段树不一定是一颗满二叉树,这点跟用户提供的元素个数有关。(如下图用户提供10个元素我们封装成的线段树)
3、线段树是一颗平衡二叉树。其实之前我们学过的堆也是一颗平衡二叉树
4、线段树不一定是完全二叉树,这点跟线段树的元素个数有关。

在这里插入图片描述

10个元素的线段树
2、平衡二叉树简介

一棵二叉树中树的最大深度与树的最小深度的差值不超过1。则这棵树为平衡二叉树。

3、假设有n个元素,使用数组表示线段树要申请多大的空间?

用户提供了指定个数数组元素,我们把这些元素放入线段树中存储。当我们的线段树底层使用数组实现时,这个数组要申请多大的空间呢?如上图,给了A[0,7]8个元素我们组成二叉树有15个节点,这时用数组实现要申请15个空间,那么我们申请的空间有规律么?接下来我们就探讨下。

(1)满二叉树情况下需要申请空间推导

假如用户提供的元素个数为n,我们正好可构成满二叉树,如下栗子:

在这里插入图片描述

特殊情况下,即用户提供元素个数为n 时,这时我们只需申请2n的空间即可。
注意:
1、这种特殊的情况为n=2^h,即n的数值为2的h次方。
2、满足 n = 2^h时,正好可构成满二叉树。

(2)非满二叉树情况下需要申请空间推导

我们知道用户提供的元素为n,n不一定满足 n = 2^h,有可能 n = 2 ^h +k这时就可能在叶子节点就多出几个元素了。如下图:这时我们还是可以吧这颗树当做满二叉树,处理的,吧空余的位置都看做空元素即可。

在这里插入图片描述
在这里插入图片描述

为啥申请4n个空间:
这里考虑到最坏的情况,n = 2^h +k,多出来一层,本来 n=2 ^h时需要申请2n个空间,多出一层,即多了2n。

代码设计

1、线段树的构建
/**
 * Create by SunnyDay on 2020/08/15
 */
public interface Merger<E> {
    
    
    // 吧泛型E 代表的两种类型元素融合成为一种元素。
    E merge(E a, E b);
}
/**
 * Create by SunnyDay on 2020/08/12
 * 线段树,基于数组方式实现。
 */
public class SegmentTree<E> {
    
    

    private E[] data; // 内部维护用户传递过来的数组
    private E[] tree;//线段树的数组实现
    private Merger<E> merger; // 融合器,消除类型之间的兼容性。

  /**
     * 构造,用户传一个数组,我们内部维护这个数组。
     */
    @SuppressWarnings("unchecked")
    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];
//        }
        // 使用for 遍历数组,给另一个数组赋值时。系统建议使用 System.arraycopy 函数
        System.arraycopy(arr, 0, data, 0, arr.length);
        tree = (E[]) new Object[4 * arr.length]; // 申请数组元素四倍空间
        // 默认情况下根节点的索引为0,区间左右端点为[0,data.length-1]
        buildSegmentTree(0, 0, data.length - 1);
    }

  /**
     * 返回完全二叉树中 给定索引所代表元素左孩子节点的索引
     */
    private int leftChild(int index) {
    
    
        return index * 2 + 1;// 公式 参考推导图
    }

    /**
     * 返回完全二叉树中 给定索引所代表元素有孩子节点的索引
     */
    private int rightChild(int index) {
    
    
        return index * 2 + 2;// 公式 参考推导图
    }
       public int getSize() {
    
    
        return data.length;
    }

    /**
     * 获得指定索引的元素
     */
    public E get(int index) {
    
    
        if (0 < index || index >= data.length) {
    
    
            throw new IllegalArgumentException("index is illegal");
        }
        return data[index];
    }
}
  /**
     * 在treeIndex 位置 创建区间为[left,right]的线段树
     */
    private void buildSegmentTree(int treeIndex, int left, int right) {
    
    
        // 1、递归终结条件(递归到底,区间就一个元素)
        //(1)找到底的条件,写判断。
        //(2)return
        if (left == right) {
    
    
         // left 代表数组索引区间,treeIndex代表 线段树数组表示中的索引位置
            tree[treeIndex] = data[left];//data[right] 意思一样
            return;
        }
        //2、区间元素为多个时,treeIndex 有左右孩子。
        int leftTreeIndex = leftChild(treeIndex);// 左孩子索引
        int rightTreeIndex = rightChild(treeIndex);// 右孩子索引
        // (1)总的区间有了,则中点也可找出。
        //int middle = (left + right) / 2;// 可能会整型溢出
        int middle = left + (right - left) / 2;
        // (2)treeIndex 位置子孩子的区间也就可标识出来了即:[left,middle],[middle+1,right]
        // (3) 有了索引,区间表示,则可递归创建左右子树作为线段树。
        buildSegmentTree(leftTreeIndex, left, middle);
        buildSegmentTree(rightTreeIndex, middle + 1, right);

        // treeIndex 索引对应区间元素和则为其左右子树元素之和
        //tree[treeIndex] = tree[leftTreeIndex] + tree[rightTreeIndex];
        //Operator '+' cannot be applied to 'E', 'E'
        //思考:+ 的使用范围应该是同种类型。
//        Object a = 10;
//        Object b  = "a";
//        Object c = a+b;

        // 上面不仅出现类型兼容问题,而且+的处理过于局限,用户只能处理区间之和。这里使用接口融合器
        // 消除兼容问题,并且业务逻辑用户自己实现。求和,区间极值都可。
        tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
    }

注意点:
1、用户指定一个数组后,其实线段或者区间就已经确定
2、由于树的天然递归性,可采用递归实现。
3、留意融合器接口的设计
4、留意两整数求平均值溢出计算

2、查询设计
  /**
     * @param queryL
     * @param queryR
     * @function 用户要查询的区间[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,区间为[left,right] 中查询[queryL,queryR] 区间
     */
    private E query(int treeIndex, int left, int right, int queryL, int queryR) {
    
    
        // 1、递归终结条件
        if (left == queryL && right == queryR) {
    
    
            return tree[treeIndex];
        }
        //2、划分区间
        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);
        int middle = left + (right - left) / 2;

        // 3、判断区间
        if (queryL >= middle + 1) {
    
     //[queryL,queryR] 区间在[left,right] 去见的右孩子区间
            return query(rightTreeIndex, middle + 1, right, queryL, queryR);
        } else if (queryR <= middle) {
    
    //[queryL,queryR] 区间在[left,right] 去见的左孩子区间
            return query(leftTreeIndex, left, middle, queryL, queryR);
        } else {
    
    //[queryL,queryR] 区间在[left,right] 区间的左右孩子都有
            E leftResult = query(leftTreeIndex, left, middle, queryL, middle);
            E rightResult = query(rightTreeIndex, middle + 1, right, middle + 1, queryR);
            return merger.merge(leftResult, rightResult);
        }
    }
3、更新操作
    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 为根节点,区间为[left,right] 内索引为 index 的元素
     */
    private void set(int treeIndex, int left, int right, int index, E e) {
    
    
        if (left == right) {
    
    
            tree[index] = e;
            return;
        }
        //2、划分区间
        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);
        int middle = left + (right - left) / 2;
        if (index>=middle+1){
    
    
            set(rightTreeIndex,middle+1,right,index,e);
        }else  {
    
    
            set(leftTreeIndex,left,middle,index,e);
        }
        // 更新
        tree[treeIndex] =merger.merge(tree[leftTreeIndex],tree[rightTreeIndex]);
    }
4、输出
   @Override
    public String toString() {
    
    
        StringBuilder sb = new StringBuilder();
        sb.append("[");
        for (int i = 0; i < tree.length; i++) {
    
    
            if (null != tree[i]) {
    
    
                sb.append(tree[i]);
            } else {
    
    
                sb.append("null");
            }
            if (i != tree.length - 1) {
    
    
                sb.append(",");
            } else {
    
    
                sb.append("]");
            }
        }
        return sb.toString();
    }

end

源码

简单的总结下,收获颇丰,溜溜球!

猜你喜欢

转载自blog.csdn.net/qq_38350635/article/details/108111009