Estructura de datos del árbol de segmentos (SegmentTree)

El árbol del segmento de línea es una estructura de datos avanzada y, por lo general, no tenemos acceso a él, pero su aparición resuelve un problema específico. Vale la pena aprender el diseño de esta estructura de datos.

Tabla de contenido

Inserte la descripción de la imagen aquí

Análisis y expansión

1. El diseño del árbol de segmento de línea

Dado un conjunto de elementos de matriz:
A [0], A [1], A [2], A [3], A [4], A [5], A [6], A [7]
Estas matrices se utilizan El intervalo se puede expresar como A [0… 7], luego podemos mantener [0,7] este intervalo se divide y se redondea continuamente, y finalmente se obtiene la forma que se muestra a continuación. Este es nuestro modelo de árbol de segmento de línea.

Inserte la descripción de la imagen aquí

1. Como se muestra en la figura anterior, cada nodo almacena un segmento de línea, o puede decirse que es un intervalo de datos.
2. El árbol del segmento de línea no es necesariamente un árbol binario completo, que está relacionado conUsuario proporcionadoEl número de elementos está relacionado. (Como se muestra en la figura siguiente, el usuario proporciona un árbol de segmento de línea que encapsulamos con 10 elementos)
3.El árbol del segmento de línea es un árbol binario equilibrado. De hecho, lo que aprendimos antesHeap también es un árbol binario equilibrado.
4. El árbol de segmento de línea no es necesariamente un árbol binario completo, esto está relacionado con el número de elementos en el árbol de segmento de línea.

Inserte la descripción de la imagen aquí

Árbol de segmento de línea de 10 elementos
2. Introducción al árbol binario equilibrado

La diferencia entre la profundidad máxima de un árbol binario y la profundidad mínima del árbol no supera 1. Entonces este árbol es un árbol binario equilibrado.

3. Suponiendo que hay n elementos, ¿cuánto espacio se debe aplicar para usar una matriz para representar el árbol del segmento de línea?

El usuario proporciona un número específico de elementos de matriz y los almacenamos en el árbol del segmento de línea. Cuando la capa inferior de nuestro árbol de segmento de línea se implementa utilizando una matriz, ¿cuánto espacio debería solicitar esta matriz? Como se muestra en la figura anterior, dados A [0,7] 8 elementos, formamos un árbol binario con 15 nodos. En este momento, necesitamos solicitar 15 espacios para la implementación con matrices, entonces ¿el espacio que solicitamos es regular? A continuación lo discutiremos.

(1) Cuando el árbol binario está lleno, debe solicitar la derivación espacial

Si el número de elementos proporcionados por el usuario es n, podemos formar un árbol binario completo, como se muestra a continuación:

Inserte la descripción de la imagen aquí

En circunstancias especiales, es decir, cuando el número de elementos proporcionados por el usuario es n, solo necesitamos solicitar 2n espacio.
Nota:
1. Este caso especial es n = 2 ^ h, es decir, el valor de n es 2 elevado a la potencia de h.
2. Cuando se satisface n = 2 ^ h, se puede formar un árbol binario completo.

(2) Necesidad de solicitar la derivación espacial en el caso de un árbol binario no completo

Sabemos que el elemento proporcionado por el usuario es n, y n no necesariamente satisface n = 2 ^ h. Es posible que n = 2 ^ h + k, entonces puede haber algunos elementos más en el nodo hoja. Como se muestra en la siguiente figura: En este momento, todavía podemos tratar este árbol como un árbol binario completo, y tratar con él, y todas las posiciones libres se pueden considerar como elementos vacíos.

Inserte la descripción de la imagen aquí
Inserte la descripción de la imagen aquí

Por qué solicitar espacios de 4n:
considerando el peor de los casos, n = 2 ^ h + k, se necesita una capa más. Originalmente, cuando n = 2 ^ h, debe solicitar espacios de 2n. Una capa más son 2n más. .

Diseño de código

1. Construcción del árbol de segmento de línea
/**
 * 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]);
    }

Nota:
1. Después de que el usuario especifica una matriz, se determina realmente el segmento de línea o intervalo
2. Debido a la recursividad natural del árbol, se puede implementar mediante recursividad.
3. Preste atención al diseño de la interfaz del dispositivo de fusión
4. Preste atención al cálculo de desbordamiento del promedio de dos enteros

2. Diseño de consultas
  /**
     * @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. Operación de actualización
    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. Salida
   @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();
    }

fin

Código fuente

En resumen, ¡la cosecha es bastante gratificante, yo-yo!

Supongo que te gusta

Origin blog.csdn.net/qq_38350635/article/details/108111009
Recomendado
Clasificación