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 implementar la ordenación por montón . [2]
Un montón binario se define como un árbol binario con dos restricciones adicionales: [3]
Los montículos en los que la clave principal es mayor o igual que (≥) las claves secundarias se denominan montículos máximos ; aquellos en los que es menor o igual que (≤) se denominan montículos mínimos . Se conocen algoritmos eficientes (es decir, de tiempo logarítmico ) para las dos operaciones necesarias para implementar una cola de prioridad en un montículo binario:
Los montones binarios también se emplean comúnmente en el algoritmo de ordenamiento 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 utilizando sus posiciones relativas dentro de esa matriz para representar relaciones padre-hijo.
Tanto la operación de inserción como la de eliminación modifican el montón para conservar primero la propiedad de forma, agregando o quitando desde el final del montón. Luego, la propiedad del montón se restaura recorriendo el montón hacia arriba o hacia abajo. Ambas operaciones toman un tiempo de O(log n ) .
Para insertar un elemento en 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 up-heap (también conocida como bubble-up , percolate-up , sift-up , trickle-up , swim-up , heapify-up , cascade-up o fix-up ).
La cantidad de operaciones requeridas depende únicamente de la cantidad de niveles que el nuevo elemento debe alcanzar 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 en el caso 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 añadir 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 ya que 15 > 11 , por lo que necesitamos intercambiar nuevamente:
que es un montón máximo válido. No hay necesidad de comprobar 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 de 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 operación down-heap (también conocida como bubble-down , percolate-down , sift-down , sink-down , trickle 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 reemplazamos 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 length ( A ). A se indexa a partir de 1.
// Realizar una operación de down-heap o heapify-down para un max-heap// A : una matriz que representa el montón, indexada a partir de 1// i : el índice en el que se debe comenzar al apilar hacia abajo 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 el mayor ≠ i entonces : intercambia A [ i ] y A [ el mayor ] Max-Heapify ( A , el mayor )
Para que el algoritmo anterior vuelva a apilar 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 apilar hacia abajo (sin el intercambio anterior) también se puede utilizar para modificar el valor de la raíz, incluso cuando no se está eliminando un elemento.
En el peor de los casos, la nueva raíz debe intercambiarse con su hijo en cada nivel hasta que alcanza el nivel inferior del montón, lo que significa que la operación de eliminación tiene una complejidad de tiempo relativa a la altura del árbol, u O(log n ).
La inserción y posterior extracción de un elemento del montón se puede realizar de manera más eficiente que simplemente invocar las funciones de inserción y extracción definidas anteriormente, lo que implicaría una operación upheap
y downheap
. En cambio, podemos realizar solo una downheap
operación, de la siguiente manera:
Python proporciona una función para la inserción y luego la extracción llamada "heappushpop", que se parafrasea a continuación. [6] [7] Se supone que la matriz de montón tiene su primer elemento en el índice 1.
// Empuja un nuevo elemento a 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 para insertar// Devuelve el mayor de los dos entre el elemento y la raíz del montón . Push-Pop ( montón : List<T>, elemento : T) -> T: si el montón no está vacío y montón[1] > elemento entonces : // < si el montón mínimo intercambia montón [1] y elemento _downheap( montón que comienza desde el índice 1) devuelve elemento
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 para insertar// Devuelve la raíz actual del montón Reemplazar ( montón : List<T>, elemento : T) -> T: intercambia montón [1] y elemento _downheap( montón que comienza desde el índice 1) devuelve elemento
Encontrar un elemento arbitrario toma O(n) tiempo.
La eliminación de un elemento arbitrario se puede realizar de la siguiente manera:
La operación de disminución de clave reemplaza el valor de un nodo con un valor dado por un valor menor, y la operación de aumento de clave hace lo mismo pero con un valor mayor. Esto implica encontrar el nodo con el valor dado, cambiar el valor y luego amontonar hacia abajo o hacia arriba para restaurar la propiedad del montón.
La disminución de la tecla se puede realizar de la siguiente manera:
El incremento de la clave se puede realizar de la siguiente manera:
Para construir un montón a partir de una matriz de n elementos de entrada, se puede empezar con un montón vacío y luego insertar sucesivamente cada elemento. Este enfoque, llamado método de Williams en honor al inventor de los montones binarios, se ejecuta fácilmente 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 estar representado por una matriz, ver más abajo). Luego, comenzando desde el nivel más bajo y moviéndose hacia arriba, tamiza la raíz de cada subárbol hacia abajo como en el algoritmo de eliminación hasta que se restablezca la propiedad de 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 corresponde a ), los árboles en la altura pueden amontonarse enviando su raíz hacia abajo a lo largo de la ruta de los hijos de valor máximo al construir un montón máximo, o los hijos de valor mínimo al construir un montón mínimo. Este proceso requiere operaciones (swaps) por nodo. En este método, la mayor parte de la amontonación tiene lugar en los niveles inferiores. Dado que la altura del montón es , la cantidad de nodos en la altura es . Por lo tanto, el costo 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 aproxima 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 mediante el uso repetido de Max-Heapify (hacia abajo el montón 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 (suponiendo que los índices comienzan en 1); por lo tanto, cada uno es un montón de un elemento y no necesita ser reducido al montón. 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 hacer: Max-Heapify ( A , i )
Los montículos se implementan comúnmente con una matriz . Cualquier árbol binario se puede almacenar en una matriz, pero debido a que un montículo binario es siempre un árbol binario completo, se puede almacenar de forma compacta. No se requiere espacio para punteros ; en cambio, el padre y los hijos de cada nodo se pueden encontrar mediante aritmética en los índices de la matriz. Estas propiedades hacen que esta implementación del montículo sea un ejemplo simple de una estructura de datos implícita o una lista Ahnentafel . Los detalles dependen de la posición de la raíz, que a su vez puede depender de las restricciones de un lenguaje de programación utilizado para la implementación o de la preferencia del programador. Específicamente, a veces la raíz se coloca en el índice 1, para simplificar la aritmética.
Sea n el número de elementos en el montón e i 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 de 0 a 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 de ordenación por heap , que reutiliza el espacio asignado a la matriz de entrada para almacenar el heap (es decir, el algoritmo se realiza en el lugar ). Esta implementación también es útil como cola de prioridad . Cuando se utiliza una matriz dinámica , es posible la inserción de una cantidad ilimitada de elementos.
Las operaciones upheap
or downheap
pueden entonces enunciarse en términos de una matriz de la siguiente manera: supongamos que la propiedad heap se cumple para los índices b , b +1, ..., e . La función sift-down extiende la propiedad heap a b −1, b , b +1, ..., e . Solo el índice i = b −1 puede violar la propiedad heap. Sea j el índice del hijo más grande de a [ i ] (para un max-heap, o el hijo más pequeño para un min-heap) dentro del rango b , ..., e . (Si no existe tal índice porque 2 i > e entonces la propiedad heap se cumple para el rango recientemente extendido y no es necesario hacer nada). Al intercambiar los valores a [ i ] y a [ j ] se establece la propiedad heap para la posición i . En este punto, el único problema es que la propiedad heap podría no cumplirse para el índice j . La función sift-down se aplica de forma recursiva de cola al índice j hasta que se establezca la propiedad heap para todos los elementos.
La función de tamizado es rápida. En cada paso solo necesita dos comparaciones y un swap. El valor del índice en el que está trabajando se duplica en cada iteración, por lo que se requieren como máximo 2 pasos log .
En el caso de grandes montones y con memoria virtual , almacenar elementos en una matriz según 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 la cantidad de páginas a las que se accede hasta en un factor de diez. [12]
La operación de fusionar dos montones binarios toma Θ( n ) para montones de igual tamaño. Lo mejor que puede hacer es (en caso de implementación de matriz) simplemente concatenar las dos matrices de montones y construir un montón del resultado. [13] Un montón en n elementos se puede fusionar con un montón en k elementos usando comparaciones de claves O(log n log k ) o, en 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 en n elementos en dos montones en k y nk elementos, respectivamente, basado en una nueva vista de los montones como colecciones ordenadas de submontones. [15] El algoritmo requiere comparaciones O(log n * log n ). 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 los 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 con la búsqueda del 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 "enhebrado" del á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 tanto del elemento más pequeño como del más grande en el tiempo. [16] Para ello, las filas se alternan entre el montón mínimo y el máximo. Los algoritmos son aproximadamente los mismos, pero, en cada paso, se deben considerar las filas alternadas con comparaciones alternadas. 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 el padre de un nodo se pueden ubicar mediante operaciones aritméticas simples en el índice del nodo. En esta sección se derivan las ecuaciones pertinentes para los montones con su raíz en el índice 0, con notas adicionales sobre los 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, .
Sea el nodo i ubicado en el nivel L y observe que cualquier nivel l contiene exactamente nodos. Además, hay exactamente nodos contenidos en las capas hasta la capa l inclusive (piense en 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 .
Sea j nodos después del nodo i en la capa L, tales que
Cada uno de estos j nodos debe tener exactamente 2 hijos, por lo que debe haber nodos que separen al hijo derecho de i del final de su capa ( ).
Observando que el hijo izquierdo de cualquier nodo está siempre 1 lugar antes de su hijo derecho, obtenemos .
Si la raíz se encuentra en el índice 1 en lugar de 0, el último nodo de cada nivel se encuentra en el índice . Si se utiliza esto en todo el proceso, se obtiene y para los montículos con su raíz en 1.
Cada nodo que no es raíz es el hijo izquierdo o derecho de su padre, por lo que debe cumplirse una de las siguientes condiciones:
Por eso,
Consideremos ahora la expresión .
Si el nodo es un hijo izquierdo, se obtiene el resultado inmediatamente; sin embargo, también se obtiene el resultado correcto si el nodo es un hijo derecho. En este caso, debe ser par y, por lo tanto, 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 el orden de los hermanos en un montón no está especificado por la propiedad heap, los dos hijos de un solo nodo se pueden intercambiar libremente a menos que al hacerlo se viole la propiedad shape (comparar con treap ). Sin embargo, tenga en cuenta que en el montón común basado en matrices, simplemente intercambiar los hijos también podría requerir mover los nodos del subárbol de los hijos para conservar la propiedad heap.
El montón binario es un caso especial del montón d-ario en el que d = 2.
Aquí se muestran las complejidades temporales [17] de varias estructuras de datos de montón. La abreviatura am. indica que la complejidad dada se amortiza, de lo contrario es una complejidad del peor caso. Para el significado de " O ( f )" y " Θ ( f )", consulte la notación Big O. Los nombres de las operaciones suponen un montón mínimo.
Porter y Simon [171] analizaron el coste medio de insertar un elemento aleatorio en un montón aleatorio en términos de intercambios. Demostraron que este promedio está limitado por la constante 1,61. Su prueba no se puede generalizar a secuencias de inserciones, ya que las inserciones aleatorias en montones aleatorios no crean montones aleatorios. El problema de inserción repetida fue resuelto por Bollobas y Simon [27]; muestran que el número esperado de intercambios está limitado por 1,7645. El coste en el peor de los casos de las inserciones y eliminaciones fue estudiado por Gonnet y Munro [84]; dan límites log log n + O(1) y log n + log n* + O(1) para el número de comparaciones respectivamente.
heapq.heappushpop(heap, item): inserta un elemento en el montón, luego extrae 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().