Data structure of the segment tree (SegmentTree)

The line segment tree is an advanced data structure, and generally we don't have access to it at all, but its emergence solves a specific problem. The design of this data structure is worth learning.

table of Contents

Insert picture description here

Analysis and expansion

1. The design of the line segment tree

Given a set of array elements:
A[0], A[1], A[2], A[3], A[4], A[5], A[6], A[7]
These arrays are used The interval can be expressed as A[0…7], then we can keep [0,7] this interval is divided and rounded continuously, and finally got the form as shown below. This is our line segment tree model.

Insert picture description here

1. As shown in the figure above, each node stores a line segment, or can be said to be an interval of data.
2. The line segment tree is not necessarily a full binary tree, which is related toUser providedThe number of elements is related. (As shown in the figure below, the user provides a line segment tree that we encapsulate with 10 elements)
3.The line segment tree is a balanced binary tree. Actually what we learned beforeHeap is also a balanced binary tree.
4. The line segment tree is not necessarily a complete binary tree. This is related to the number of elements in the line segment tree.

Insert picture description here

10-element line segment tree
2. Introduction to Balanced Binary Tree

The difference between the maximum depth of a binary tree and the minimum depth of the tree does not exceed 1. Then this tree is a balanced binary tree.

3. Assuming there are n elements, how much space should be applied for using an array to represent the line segment tree?

The user provides a specified number of array elements, and we store these elements in the line segment tree. When the bottom layer of our line segment tree is implemented using an array, how much space should this array apply for? As shown in the figure above, given A[0,7] 8 elements, we form a binary tree with 15 nodes. At this time, we need to apply for 15 spaces for implementation with arrays, so is the space we apply for regular? Next we will discuss it.

(1) When the binary tree is full, it needs to apply for space derivation

If the number of elements provided by the user is n, we can form a full binary tree, as shown below:

Insert picture description here

Under special circumstances, that is, when the number of elements provided by the user is n, then we only need to apply for 2n space.
Note:
1. This special case is n=2^h, that is, the value of n is 2 to the power of h.
2. When n = 2^h is satisfied, a full binary tree can be formed.

(2) In the case of a non-full binary tree, it is necessary to apply for space derivation

We know that the element provided by the user is n, and n does not necessarily satisfy n = 2^h. It is possible that n = 2 ^h + k, then there may be a few more elements in the leaf node. As shown in the figure below: At this time, we can still treat this tree as a full binary tree, and deal with it, and all the spare positions can be regarded as empty elements.

Insert picture description here
Insert picture description here

Why apply for 4n spaces:
Considering the worst case here, n = 2^h +k, one more layer is needed. Originally, when n=2 ^h, you need to apply for 2n spaces, and one more layer is 2n more. .

Code design

1. Construction of line segment tree
/**
 * 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]);
    }

Note:
1. After the user specifies an array, the line segment or interval is actually determined.
2. Due to the natural recursion of the tree, it can be implemented by recursion.
3. Pay attention to the design of the fusion device interface
4. Pay attention to the overflow calculation of the average of two integers

2. Query design
  /**
     * @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. Update operation
    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. Output
   @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

Source code

In a simple summary, the harvest is quite rewarding, yo-yo!

Guess you like

Origin blog.csdn.net/qq_38350635/article/details/108111009