[DSA] Explicación detallada montón-montón (tome el montón más grande como ejemplo)

Montón

[Definición]
Heap es un término general para un tipo especial de estructura de datos en informática. El montón suele ser un objeto de matriz que se puede ver como un árbol binario completo.

【Nota】

  • El montón mencionado aquí es una estructura de datos, no el concepto de montón en el modelo de memoria.
  • El montón aquí es una estructura lógica.

【Naturaleza】

  • El valor de cualquier nodo en el montón no siempre es mayor que (no menor que) el valor de sus nodos secundarios;
  • La pila es siempre un árbol completo.

【Descripción】

  • El montón con el nodo raíz más grande se llama el montón más grande o el montón raíz grande , y el montón con el nodo raíz más pequeño se llama el montón más pequeño o el montón raíz más pequeño . Los montones comunes incluyen horquillas binarias y montones de Fibonacci.
  • El montón es una estructura de datos no lineal, equivalente a una matriz unidimensional, con dos sucesores directos.

Tenedor binario

El montón binario es un árbol binario completo o un árbol binario completo aproximado, se divide en dos tipos: montón máximo y montón mínimo.

Árbol binario completo : si la profundidad del árbol binario es h, el número de nodos en todas las otras capas (1 ~ h-1) excepto la capa h-th alcanza el número máximo, y todos los nodos en la capa h-th se concentran continuamente en el extremo izquierdo, Este es un árbol binario completo. Como se muestra en la figura a continuación, son árboles completamente binarios.
Inserte la descripción de la imagen aquí
Montón máximo: el valor clave del nodo primario siempre es mayor o igual que el valor clave de cualquier nodo secundario;
montón mínimo: el valor clave del nodo primario siempre es menor o igual que el valor clave de cualquier nodo secundario.
El diagrama esquemático es el siguiente:Inserte la descripción de la imagen aquí

Implementación de almacenamiento dinámico binario

Los montones binarios generalmente se implementan a través de " matrices ". El montón binario realizado por la matriz tiene una cierta relación entre la posición del nodo primario y el nodo secundario. A veces, colocamos el "primer elemento del montón binario" en la posición del índice de matriz 0, y a veces en la posición de 1. Por supuesto, son esencialmente lo mismo (ambos montones binarios), pero hay una ligera diferencia en la implementación.

[Nota] ¡La implementación de la bifurcación binaria en este artículo adopta el método de "el primer elemento de la bifurcación binaria en el índice de matriz es 0"!

El gran montón de root en la figura anterior tiene dos métodos de implementación:

  1. El primer elemento se coloca en el índice 0.
    En este punto, la relación entre la tabla de matriz y los nodos es la siguiente:
  • El índice de matriz del elemento secundario izquierdo con índice i es (2 * i + 1)
  • El índice de matriz del elemento secundario derecho con índice i es (2 * i + 2)
  • El índice de matriz del nodo padre con índice i es ((i-1) / 2)
    comprensión intuitiva:
    cuando el índice de matriz es 0, el nodo padre es un [0], el hijo izquierdo es un [1] y el hijo derecho es a [2]
    Cuando el subíndice de la matriz es 1, el nodo padre es un [0], el hijo izquierdo es un [3] y el hijo derecho es un [4]
    Cuando el subíndice de la matriz es 2, el nodo padre es un [0] , El niño izquierdo es un [5], el niño derecho es un [6]
    Inserte la descripción de la imagen aquí
  1. El primer elemento se coloca en el índice 1
  • El índice de matriz del elemento secundario izquierdo con índice i es (2 * i)
  • El índice de matriz del elemento secundario derecho con índice i es (2 * i + 1)
  • El índice de matriz del nodo primario con índice i es (2/2),
    que no se repetirá aquí.
    Inserte la descripción de la imagen aquí

Operación de montón binario

El núcleo del método de operación de bifurcación binaria es [agregar nodo], [eliminar nodo]. Los siguientes ejemplos han tomado el montón de raíz grande como ejemplo.

Agregar diagrama de nodo

Insertar nodo 85
Inserte la descripción de la imagen aquí
Paso 1:
Inserte el nuevo nodo al final de la matriz.
Inserte la descripción de la imagen aquí
Paso 2:
Compare el tamaño del nodo recién insertado y el nodo padre, donde 85> 40, luego intercambie la posición con el nodo padre
Inserte la descripción de la imagen aquí
Paso 3:
Repita los pasos de comparación anteriores.
Inserte la descripción de la imagen aquí
En el paso de movimiento, se encuentra que 85 es menos de 100, luego deja de moverte.

Eliminar diagrama de nodo

Tome la eliminación del nodo raíz como ejemplo. El
primer paso: borrar los datos del nodo raíz. El
Inserte la descripción de la imagen aquí
segundo paso: mover el último nodo al nodo raíz. El
Inserte la descripción de la imagen aquí
tercer paso: comparar con los dos nodos secundarios, seleccionar el nodo secundario más grande e intercambiarlo con el
Inserte la descripción de la imagen aquí
cuarto. Paso: repite el tercer paso
Inserte la descripción de la imagen aquí

Nota: Si no está eliminando el nodo raíz, debe prestarle atención. Después de eliminar, también debe asegurarse de que el árbol reemplazado tenga un montón de raíz grande y sea un árbol binario completo.

Código de implementación

#include <stdio.h>
#include <stdlib.h>

#define ARRAY_LEN(arr) ((sizeof(arr))/sizeof(arr[0]))

#define MAX_NUM (128)

typedef int Type;

static Type heap_arr[MAX_NUM];
static int  heap_size = 0; // 堆数组的大小

/**
 * 根据数据 data 从对中获取对应的索引
 * @param  data [description]
 * @return      [description]
 */
int get_data_index_from_heap(int data)
{
    for (int i = 0; i < heap_size; ++i)
    {
        if (data == heap_arr[i])
        {
            return i;
        }
    }

    return -1;
}

/**
 * 在数组实现的堆中,向下调整元素的位置,使之符合大根堆
 * 注:   
 *     在数组试下你的堆中,第 i 个节点的
 *     左孩子的下标是 2*i+1, 
 *     右孩子的下标是 2*i+2,
 *     父节点的下标是 (i-1)/2
 *     
 * @param  start [一般从删除元素的位置开始]
 * @param  end   [数组的最后一个索引]
 */
static void max_heap_fixup_down(int start, int end)
{
    int curr_node_pos = start;
    int left_child = 2*start+1;
    int curr_node_data = heap_arr[curr_node_pos];



    while(left_child <= end) 
    {

        // left_child 是左孩子, left_child+1是同一个父节点下的右孩子
        if (left_child < end && heap_arr[left_child] < heap_arr[left_child+1])
        {
            // 从被删除的节点的左右孩子中选取较大的,赋值给父节点
            left_child++;
        }
        if (curr_node_data >= heap_arr[left_child])
        {
            // 选出孩子节点的较大者之后,与当前节点比较
            break;
        }
        else
        {
            heap_arr[curr_node_pos] = heap_arr[left_child];
            curr_node_pos = left_child;
            left_child = 2*left_child+1;
        }
    }

    heap_arr[curr_node_pos] = heap_arr[left_child];
}

/**
 * 删除对中的数据 data
 * @param data [description]
 */
static int max_heap_delete(int data)
{
    if (heap_size == 0)
    {
        printf("堆已空!\n");
        return -1;
    }
    int index = get_data_index_from_heap(data);
    if (index < 0)
    {
        printf("删除失败, 数据 [%d] 不存在!\n", data);
        return -1;
    }

    // 删除index的元素,使用最后的元素将其替换
    heap_arr[index] = heap_arr[--heap_size];

    // 删除元素之后,调整堆
    max_heap_fixup_down(index, heap_size-1);
}

/**
 * 在数组实现的堆中,将元素向上调整
 * 注:   
 *     在数组试下你的堆中,第 i 个节点的
 *     左孩子的下标是 2*i+1, 
 *     右孩子的下标是 2*i+2,
 *     父节点的下标是 (i-1)/2
 *     
 * @param  start [从数组的最后一个元素开始,start是最后一个元素的下标]
 */
static void max_heap_fixup_up(int start)
{
    int curr_node_pos = start;
    int parent = (start-1)/2;
    int curr_node_data = heap_arr[curr_node_pos];

    // 从最后一个元素开始比价,知道第0个元素
    while(curr_node_pos > 0) 
    {   
        // 当前节点的数据小于父节点,退出
        if (curr_node_data <= heap_arr[parent])
        {
            break;
        }
        else
        {
            // 交换父节点和当前节点
            heap_arr[curr_node_pos] = heap_arr[parent];
            heap_arr[parent] = curr_node_data;

            curr_node_pos = parent;
            parent = (parent-1)/2;
        }
    }
}

/**
 * 将新数据插入到二叉堆中
 * @param  data [插入数据]
 * @return      [成功返回0, 失败返回-1]
 */
int max_heap_insert(Type data)
{
    if (heap_size == MAX_NUM)
    {
        printf("堆已经满了!\n");
        return -1;
    }

    heap_arr[heap_size] = data;
    // 调整堆 
    max_heap_fixup_up(heap_size);
    heap_size++; // 对的数量自增

    return 0;
}

/**
 * 打印二叉堆
 */
void max_heap_print()
{
    for (int i = 0; i < heap_size; ++i)
    {
        printf("%d ", heap_arr[i]);
    }
}

int main(int argc, char const *argv[])
{
    Type tmp[] = {10, 40, 30, 60, 90, 70, 20, 50, 80};
    int len = ARRAY_LEN(tmp);

    printf("---> 添加元素:\n");
    for (int i = 0; i < len; ++i)
    {
        printf("%d ", tmp[i]);
        max_heap_insert(tmp[i]);
    }   

    printf("\n---> 最大堆: ");
    max_heap_print();

    max_heap_insert(85);
    printf("\n---> 插入元素之后 最大堆: ");
    max_heap_print();


    max_heap_delete(90);
    printf("\n---> 删除元素之后 最大堆: ");
    max_heap_print();
    printf("\n");

    return 0;
}

Escenarios de aplicación de montón

Tipo de montón

Hay dos procesos: construir un montón y ordenar. El proceso de construir un montón es el proceso de insertar elementos en el montón. Podemos construir el montón en la matriz original in situ y luego generar los elementos superiores del montón en secuencia para lograr el propósito de la clasificación. La complejidad de tiempo de construir un montón es O (n), y la complejidad de tiempo del proceso de clasificación es O (nlogn). La clasificación de montón no es un algoritmo de clasificación estable porque hay un intercambio del último elemento del montón con el elemento superior del montón durante el proceso de clasificación. La operación puede cambiar el orden relativo original.

Los montones se usan comúnmente para implementar colas prioritarias.

En la cola, el programador del sistema operativo extrae repetidamente el primer trabajo en la cola y lo ejecuta, porque en realidad algunas tareas cortas esperarán mucho tiempo para finalizar, o algunas tareas no cortas pero importantes , También debe tener prioridad. El montón es una estructura de datos diseñada para resolver tales problemas.
-Merge ordenó archivos pequeños

Si hay 100 archivos pequeños, cada archivo pequeño es de 100 MB, y cada archivo pequeño almacena una cadena ordenada, y ahora es necesario fusionarlo en un archivo grande ordenado, entonces, ¿cómo hacerlo?

El enfoque intuitivo es tomar la primera línea de cada archivo pequeño en la matriz, y luego comparar el tamaño e insertarlo en el archivo grande en secuencia. Si la línea más pequeña proviene del archivo a, luego eliminarla de la matriz después de insertarla en el archivo grande Esta línea, luego tome la siguiente línea de archivo a e insértela en la matriz, compare el tamaño nuevamente, tome la línea más pequeña insertada en la segunda línea del archivo grande, y así sucesivamente. Todo el proceso es muy parecido a la función de fusión de clasificación por fusión. Obviamente, es ineficiente recorrer toda la matriz cada vez que se inserta en un archivo grande.

La cola prioritaria con la ayuda del montón es muy eficiente. Por ejemplo, podemos tomar la primera línea de 100 archivos para construir un pequeño montón superior. Si el elemento superior del archivo proviene del archivo a, entonces elimine el elemento superior de la pila e insértelo en un archivo grande, y elimine el elemento de la parte superior de la pila (esta es la implementación del montón) removeMax function), y luego tome la siguiente línea del archivo a e insértela en la parte superior del montón, repita el proceso anterior para completar la operación de fusionar archivos pequeños ordenados.

La complejidad de tiempo de eliminar los datos superiores del montón e insertar datos en el montón es O (logn), donde n representa el número de datos en el montón, que aquí es 100.

-Timer temporizador de alto rendimiento

Si hay muchas tareas de tiempo, ¿cómo diseñar un temporizador de alto rendimiento para realizar estas tareas de tiempo? Si pasa cada pequeña unidad de tiempo (por ejemplo, 1 segundo), escanee la tarea nuevamente para ver si alguna tarea alcanza el tiempo de ejecución establecido. Si llega, sácalo y ejecútalo. Obviamente, esto es un desperdicio de recursos, porque el intervalo de tiempo entre estas tareas puede ser de varias horas.

Con la ayuda de la cola prioritaria del montón, podemos diseñar de esta manera: construya un pequeño montón superior en orden cronometrado, saque primero la tarea superior y consulte la diferencia entre su tiempo de ejecución y el tiempo actual. Si son T segundos, entonces En el tiempo de T-1 segundo, el temporizador no necesita hacer nada. Cuando se alcanza el intervalo de T segundos, la tarea se saca y se ejecuta. En consecuencia, el elemento superior del montón se elimina de la parte superior del montón, y luego el siguiente elemento superior del montón se elimina para consultar su ejecución Tiempo

De esta manera, no es necesario sondear el temporizador una vez cada 1 segundo, ni necesita recorrer toda la lista de tareas, y se mejora el rendimiento.

-topK cuestiones

La situación de tomar los primeros elementos k se puede dividir en dos categorías, uno es la recopilación de datos estáticos, es decir, no se agregarán elementos nuevos después de determinar los datos, y el otro es la recopilación de datos dinámicos, que agregará elementos en cualquier momento, pero aún buscará k Gran elemento

Para datos estáticos, primero podemos insertar los datos estáticos en el montón superior pequeño en secuencia, mantener un montón superior pequeño de tamaño k, atravesar los datos restantes e insertarlo en el montón superior pequeño de tamaño k en secuencia. Si el elemento es más pequeño que k, entonces Sin procesar, continúe atravesando los siguientes datos. Si es mayor que k, elimine el montón superior e inserte el valor en la parte superior del montón, de modo que al final del recorrido, el elemento superior del montón sea el k-ésimo elemento más grande.

Recorrer una matriz requiere O (n) complejidad de tiempo, y una operación de montón requiere O (logK) complejidad de tiempo, por lo que en el peor de los casos, n elementos se colocan en el montón una vez, por lo que la complejidad de tiempo es O (nlogK).

Para los datos dinámicos, el método de procesamiento también es el mismo, lo que equivale a encontrar k superior en tiempo real, luego se puede volver a calcular cada vez para encontrar k superior, y la complejidad del tiempo sigue siendo O (nlogK), n representa el tamaño de los datos actuales. Siempre podemos mantener un pequeño montón superior de tamaño K. Cuando se agregan datos a la colección, lo comparamos con los elementos en la parte superior del montón. Si es más grande que el elemento superior del montón, eliminamos el elemento superior del montón e insertamos este elemento en el montón; si es más pequeño que el elemento superior del montón, no se realiza ningún procesamiento. De esta manera, cada vez que necesitemos consultar los big data actuales de K principales, podemos volver inmediatamente a él.

134 artículos originales publicados · Me gustaron 119 · Visite 310,000+

Supongo que te gusta

Origin blog.csdn.net/jobbofhe/article/details/102555102
Recomendado
Clasificación