Un montón binario es una estructura de datos de montón que toma la forma de un árbol binario . Los montones binarios son una forma común de implementar colas de prioridad . [1] : 162–163 El montón binario fue introducido por JWJ Williams en 1964, como una estructura de datos para heapsort . [2]
Un montón binario se define como un árbol binario con dos restricciones adicionales: [3]
Los montones donde la clave principal es mayor o igual que (≥) las claves secundarias se denominan montones máximos ; aquellos en los que es menor o igual que (≤) se denominan min-heaps . Se conocen algoritmos eficientes ( tiempo logarítmico ) para las dos operaciones necesarias para implementar una cola de prioridad en un montón binario: insertar un elemento y eliminar el elemento más pequeño o más grande de un montón mínimo o máximo, respectivamente. Los montones binarios también se emplean comúnmente en el algoritmo de clasificación heapsort , que es un algoritmo in situ porque los montones binarios se pueden implementar como una estructura de datos implícita , almacenando claves en una matriz y usando sus posiciones relativas dentro de esa matriz para representar las relaciones entre padres e hijos. .
Tanto la operación de inserción como la de eliminación modifican el montón para que se ajuste primero a la propiedad de forma, agregando o eliminando desde el final del montón. Luego, la propiedad del montón se restaura recorriendo hacia arriba o hacia abajo el montón. Ambas operaciones toman O(log n ) tiempo.
Para insertar un elemento a un montón, realizamos los siguientes pasos:
Los pasos 2 y 3, que restauran la propiedad del montón comparando y posiblemente intercambiando un nodo con su padre, se denominan operación de montón (también conocida como burbuja , filtración , tamizado , goteo , natación) . up , heapify-up , cascade-up o fix-up ).
El número de operaciones necesarias depende únicamente del número de niveles que debe subir el nuevo elemento para satisfacer la propiedad del montón. Por lo tanto, la operación de inserción tiene una complejidad temporal en el peor de los casos de O(log n ) . Para un montón aleatorio y para inserciones repetidas, la operación de inserción tiene una complejidad promedio de O(1). [4] [5]
Como ejemplo de inserción de montón binario, digamos que tenemos un montón máximo
y queremos agregar el número 15 al montón. Primero colocamos el 15 en la posición marcada por la X. Sin embargo, la propiedad del montón se viola ya que 15 > 8 , por lo que necesitamos intercambiar el 15 y el 8. Entonces, tenemos el montón con el siguiente aspecto después del primer intercambio:
Sin embargo, la propiedad del montón aún se viola desde 15 > 11 , por lo que debemos intercambiar nuevamente:
que es un montón máximo válido. No es necesario verificar el hijo izquierdo después de este paso final: al principio, el montón máximo era válido, lo que significa que la raíz ya era mayor que su hijo izquierdo, por lo que reemplazar la raíz con un valor aún mayor mantendrá la propiedad que cada nodo es mayor que sus hijos ( 11 > 5 ; si 15 > 11 y 11 > 5 , entonces 15 > 5 , debido a la relación transitiva ).
El procedimiento para eliminar la raíz del montón (extrayendo efectivamente el elemento máximo en un montón máximo o el elemento mínimo en un montón mínimo) mientras se conserva la propiedad del montón es el siguiente:
Los pasos 2 y 3, que restauran la propiedad del montón comparando y posiblemente intercambiando un nodo con uno de sus hijos, se denominan montón descendente (también conocido como burbuja descendente , filtración , tamizado , hundimiento , goteo). operación down , heapify-down , cascade-down , fix-down , extract-min o extract-max , o simplemente heapify ).
Entonces, si tenemos el mismo montón máximo que antes
Quitamos el 11 y lo sustituimos por el 4.
Ahora se viola la propiedad del montón ya que 8 es mayor que 4. En este caso, intercambiar los dos elementos, 4 y 8, es suficiente para restaurar la propiedad del montón y no necesitamos intercambiar más elementos:
El nodo que se mueve hacia abajo se intercambia con el mayor de sus hijos en un montón máximo (en un montón mínimo se intercambiaría con su hijo más pequeño), hasta que satisfaga la propiedad del montón en su nueva posición. Esta funcionalidad se logra mediante la función Max-Heapify como se define a continuación en pseudocódigo para un montón A respaldado por una matriz de longitud ( A ) . A está indexado a partir de 1.
// Realizar una operación de reducción o reducción del montón para un montón máximo// A : una matriz que representa el montón, indexada a partir de 1// i : el índice para comenzar al acumular Max-Heapify ( A , i ): izquierda ← 2× i derecha ← 2× i + 1 más grande ← i si izquierda ≤ longitud ( A ) y A [ izquierda ] > A [ mayor ] entonces : mayor ← izquierda
si derecha ≤ longitud ( A ) y A [ derecha ] > A [ mayor ] entonces : mayor ← derecha si mayor ≠ i entonces : intercambiar A [ i ] y A [ mayor ] Max-Heapify ( A , mayor )
Para que el algoritmo anterior vuelva a amontonar correctamente la matriz, ningún nodo además del nodo en el índice i y sus dos hijos directos puede violar la propiedad del montón. La operación de reducción del montón (sin el intercambio anterior) también se puede utilizar para modificar el valor de la raíz, incluso cuando no se elimina un elemento.
En el peor de los casos, la nueva raíz debe intercambiarse con su hijo en cada nivel hasta que alcance el nivel inferior del montón, lo que significa que la operación de eliminación tiene una complejidad temporal relativa a la altura del árbol, u O(log n ).
Insertar un elemento y luego extraerlo del montón se puede hacer de manera más eficiente que simplemente llamar a las funciones de inserción y extracción definidas anteriormente, lo que implicaría una operación upheap
y downheap
. En cambio, podemos hacer solo una downheap
operación, de la siguiente manera:
Python proporciona una función de inserción y luego extracción llamada "heappushpop", que se parafrasea a continuación. [6] [7] Se supone que la matriz del montón tiene su primer elemento en el índice 1.
// Inserta un nuevo elemento en un montón (máximo) y luego extrae la raíz del montón resultante.// montón : una matriz que representa el montón, indexada en 1// elemento : un elemento a insertar// Devuelve el mayor de los dos entre el elemento y la raíz del montón . Push-Pop ( montón : Lista<T>, elemento : T) -> T: si el montón no está vacío y el montón[1] > elemento entonces : // < si el montón mínimo intercambia el montón [1] y el elemento _downheap( inicio del montón del índice 1) devolver artículo
Se puede definir una función similar para extraer y luego insertar, que en Python se llama "heapreplace":
// Extrae la raíz del montón y envía un nuevo elemento// montón : una matriz que representa el montón, indexada en 1// elemento : un elemento a insertar// Devuelve la raíz actual del montón Reemplazar ( montón : Lista<T>, elemento : T) -> T: intercambiar montón [1] y elemento _downheap( montón a partir del índice 1) devolver elemento
Encontrar un elemento arbitrario lleva O(n) tiempo.
La eliminación de un elemento arbitrario se puede realizar de la siguiente manera:
La operación de tecla de disminución reemplaza el valor de un nodo con un valor dado con un valor más bajo, y la operación de tecla de aumento hace lo mismo pero con un valor más alto. Esto implica encontrar el nodo con el valor dado, cambiar el valor y luego reducir o aumentar el almacenamiento para restaurar la propiedad del montón.
La tecla de disminución se puede hacer de la siguiente manera:
La clave de aumento se puede hacer de la siguiente manera:
Se puede construir un montón a partir de una matriz de n elementos de entrada comenzando con un montón vacío y luego insertando sucesivamente cada elemento. Se ve fácilmente que este enfoque, llamado método de Williams en honor al inventor de los montones binarios, se ejecuta en tiempo O ( n log n ) : realiza n inserciones a un costo O (log n ) cada una. [a]
Sin embargo, el método de Williams no es óptimo. Un método más rápido (debido a Floyd [8] ) comienza colocando arbitrariamente los elementos en un árbol binario, respetando la propiedad de forma (el árbol podría representarse mediante una matriz, ver más abajo). Luego, comenzando desde el nivel más bajo y avanzando hacia arriba, tamice la raíz de cada subárbol hacia abajo como en el algoritmo de eliminación hasta que se restablezca la propiedad del montón. Más específicamente, si todos los subárboles que comienzan en cierta altura ya han sido "amontonados" (el nivel más bajo correspondiente a ), los árboles en altura se pueden amontonar enviando su raíz hacia abajo a lo largo del camino de los hijos con valor máximo al construir un montón máximo, o niños con valor mínimo al construir un montón mínimo. Este proceso requiere operaciones (intercambios) por nodo. En este método la mayor parte de la amontonación tiene lugar en los niveles inferiores. Como la altura del montón es , el número de nodos en la altura es . Por tanto, el coste de amontonar todos los subárboles es:
Esto utiliza el hecho de que la serie infinita dada converge .
Se sabe que el valor exacto de lo anterior (el número de comparaciones en el peor de los casos durante la construcción del montón) es igual a:
donde s 2 ( n ) es la suma de todos los dígitos de la representación binaria de n y e 2 ( n ) es el exponente de 2 en la factorización prima de n .
El caso promedio es más complejo de analizar, pero se puede demostrar que se acerca asintóticamente a 1,8814 n − 2 log 2 n + O (1) comparaciones. [10] [11]
La función Build-Max-Heap que sigue convierte una matriz A que almacena un árbol binario completo con n nodos en un montón máximo usando repetidamente Max-Heapify (hacia abajo para un montón máximo) de manera ascendente. . Los elementos de la matriz indexados por Floor ( n /2) + 1 , Floor ( n /2) + 2 , ..., n son todas hojas del árbol (asumiendo que los índices comienzan en 1); por lo tanto, cada uno es un elemento. montón, y no es necesario reducirlo. Build-Max-Heap ejecuta Max-Heapify en cada uno de los nodos restantes del árbol.
Build-Max-Heap ( A ): para cada índice i desde el piso ( longitud ( A )/2) hasta 1 haga: Max-Heapify ( A , i )
Los montones se implementan comúnmente con una matriz . Cualquier árbol binario se puede almacenar en una matriz, pero como un montón binario es siempre un árbol binario completo, se puede almacenar de forma compacta. No se requiere espacio para los punteros ; en cambio, el padre y los hijos de cada nodo se pueden encontrar mediante aritmética en índices de matriz. Estas propiedades hacen de esta implementación del montón un ejemplo simple de una estructura de datos implícita o lista de Ahnentafel . Los detalles dependen de la posición de la raíz, que a su vez puede depender de las limitaciones del lenguaje de programación utilizado para la implementación o de las preferencias del programador. En concreto, a veces la raíz se sitúa en el índice 1, para simplificar la aritmética.
Sea n el número de elementos en el montón y i sea un índice válido arbitrario de la matriz que almacena el montón. Si la raíz del árbol está en el índice 0, con índices válidos del 0 al n − 1, entonces cada elemento a en el índice i tiene
Alternativamente, si la raíz del árbol está en el índice 1, con índices válidos del 1 al n , entonces cada elemento a en el índice i tiene
Esta implementación se utiliza en el algoritmo heapsort que reutiliza el espacio asignado a la matriz de entrada para almacenar el montón (es decir, el algoritmo se realiza in situ ). Esta implementación también es útil como cola de prioridad . Cuando se utiliza una matriz dinámica , es posible la inserción de un número ilimitado de elementos.
Las operaciones upheap
o downheap
se pueden expresar en términos de una matriz de la siguiente manera: supongamos que la propiedad del montón se cumple para los índices b , b +1, ..., e . La función de filtrado extiende la propiedad del montón a b −1, b , b +1, ..., e . Sólo el índice i = b −1 puede violar la propiedad del montón. Sea j el índice del hijo más grande de a [ i ] (para un montón máximo, o el hijo más pequeño para un montón mínimo) dentro del rango b , ..., e . (Si no existe tal índice porque 2 i > e entonces la propiedad del montón se mantiene para el rango recién extendido y no es necesario hacer nada). Al intercambiar los valores a [ i ] y a [ j ], se establece la propiedad del montón para la posición i . En este punto, el único problema es que es posible que la propiedad del montón no se cumpla para el índice j . La función de filtrado se aplica de forma recursiva al índice j hasta que se establece la propiedad del montón para todos los elementos.
La función de filtrado es rápida. En cada paso sólo se necesitan dos comparaciones y un intercambio. El valor del índice donde está trabajando se duplica en cada iteración, por lo que como máximo se requieren log 2 e pasos.
Para grandes montones y que utilizan memoria virtual , almacenar elementos en una matriz de acuerdo con el esquema anterior es ineficiente: (casi) cada nivel está en una página diferente . Los montones B son montones binarios que mantienen subárboles en una sola página, lo que reduce el número de páginas a las que se accede hasta en un factor de diez. [12]
La operación de fusionar dos montones binarios requiere Θ( n ) para montones del mismo tamaño. Lo mejor que puede hacer es (en el caso de una implementación de matriz) simplemente concatenar las dos matrices del montón y crear un montón del resultado. [13] Un montón de n elementos se puede fusionar con un montón de k elementos usando comparaciones de claves O(log n log k ) o, en el caso de una implementación basada en punteros, en tiempo O(log n log k ). [14] En se presentó un algoritmo para dividir un montón de n elementos en dos montones de k y nk elementos, respectivamente, basado en una nueva visión de los montones como colecciones ordenadas de submontones. [15] El algoritmo requiere O(log n * log n ) comparaciones. La vista también presenta un algoritmo nuevo y conceptualmente simple para fusionar montones. Cuando la fusión es una tarea común, se recomienda una implementación de montón diferente, como montones binomiales , que se pueden fusionar en O(log n ).
Además, se puede implementar un montón binario con una estructura de datos de árbol binario tradicional, pero existe un problema al encontrar el elemento adyacente en el último nivel del montón binario al agregar un elemento. Este elemento se puede determinar algorítmicamente o agregando datos adicionales a los nodos, lo que se denomina "enhebrar" el árbol; en lugar de simplemente almacenar referencias a los hijos, también almacenamos el sucesor en orden del nodo.
Es posible modificar la estructura del montón para hacer posible la extracción del elemento más pequeño y más grande en el tiempo. [16] Para hacer esto, las filas alternan entre montón mínimo y montón máximo. Los algoritmos son más o menos los mismos, pero, en cada paso, se deben considerar las filas alternas con comparaciones alternas. El rendimiento es aproximadamente el mismo que el de un montón normal de una sola dirección. Esta idea se puede generalizar a un montón mínimo-máximo-mediano.
En un montón basado en matrices, los hijos y los padres de un nodo se pueden ubicar mediante aritmética simple en el índice del nodo. Esta sección deriva las ecuaciones relevantes para montones con su raíz en el índice 0, con notas adicionales sobre montones con su raíz en el índice 1.
Para evitar confusiones, definimos el nivel de un nodo como su distancia desde la raíz, de modo que la raíz misma ocupa el nivel 0.
Para un nodo general ubicado en el índice i (comenzando desde 0), primero derivaremos el índice de su hijo derecho .
Supongamos que el nodo i esté ubicado en el nivel L y tenga en cuenta que cualquier nivel l contiene exactamente nodos. Además, hay exactamente nodos contenidos en las capas hasta la capa l inclusive (piense en la aritmética binaria; 0111...111 = 1000...000 - 1). Debido a que la raíz se almacena en 0, el késimo nodo se almacenará en el índice . Al juntar estas observaciones se obtiene la siguiente expresión para el índice del último nodo en la capa l .
Sean j nodos después del nodo i en la capa L, tales que
Cada uno de estos nodos j debe tener exactamente 2 hijos, por lo que debe haber nodos que separen al hijo derecho de i del final de su capa ( ).
Si observamos que el hijo izquierdo de cualquier nodo siempre está 1 lugar antes que su hijo derecho, obtenemos .
Si la raíz está ubicada en el índice 1 en lugar de 0, el último nodo de cada nivel está en el índice . Usar esto en todos los rendimientos y en montones con su raíz en 1.
Cada nodo no raíz es hijo izquierdo o derecho de su padre, por lo que debe cumplirse uno de los siguientes:
Por eso,
Consideremos ahora la expresión .
Si el nodo es un hijo izquierdo, esto da el resultado inmediatamente; sin embargo, también da el resultado correcto si el nodo es un hijo derecho. En este caso, debe ser par y, por tanto, debe ser impar.
Por lo tanto, independientemente de si un nodo es hijo izquierdo o derecho, su padre se puede encontrar mediante la expresión:
Dado que la propiedad del montón no especifica el orden de los hermanos en un montón, los dos hijos de un solo nodo se pueden intercambiar libremente a menos que al hacerlo se viole la propiedad de forma (compárese con treap ). Sin embargo, tenga en cuenta que en el montón común basado en matrices, el simple hecho de intercambiar los elementos secundarios también podría requerir mover los nodos del subárbol de los elementos secundarios para conservar la propiedad del montón.
El montón binario es un caso especial del montón d-ario en el que d = 2.
A continuación se presentan las complejidades temporales [17] de varias estructuras de datos del montón. Los nombres de las funciones suponen un montón mínimo. Para conocer el significado de " O ( f )" y " Θ ( f )", consulte la notación O grande .
Porter y Simon [171] analizaron el costo promedio de insertar un elemento aleatorio en un montón aleatorio en términos de intercambios. Demostraron que este promedio está acotado por la constante 1,61. Su prueba no se generaliza a secuencias de inserciones, ya que las inserciones aleatorias en montones aleatorios no crean montones aleatorios. El problema de la inserción repetida fue resuelto por Bollobas y Simon [27]; muestran que el número esperado de intercambios está limitado por 1,7645. Gonnet y Munro [84] estudiaron el peor coste de las inserciones y eliminaciones; dan límites log log n + O(1) y log n + log n* + O(1) para el número de comparaciones, respectivamente.
heapq.heappushpop(montón, elemento): inserta el elemento en el montón, luego saca y devuelve el elemento más pequeño del montón. La acción combinada se ejecuta de manera más eficiente que heappush() seguida de una llamada separada a heappop().