データ構造セグメントツリー(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、もう1つのレイヤーが必要です。元々、n = 2 ^ hの場合、2nスペースを申請する必要があります。もう1つのレイヤーは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つの整数の平均のオーバーフロー計算に注意してください。

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

終わり

ソースコード

簡単に言えば、収穫は非常にやりがいがあります、ヨーヨー!

おすすめ

転載: blog.csdn.net/qq_38350635/article/details/108111009