En informática , heapsort es un algoritmo de ordenación eficiente basado en comparaciones que reorganiza un array de entrada en un montón (una estructura de datos donde cada nodo es mayor que sus hijos) y luego elimina repetidamente el nodo más grande de ese montón, colocándolo al final del array de manera similar a Selection sort . [ 3 ]
Aunque en la práctica es algo más lento en la mayoría de las máquinas que un quicksort bien implementado , tiene las ventajas de una implementación muy simple y un tiempo de ejecución en el peor de los casos más favorable, O ( n log n ) . La mayoría de las variantes de quicksort del mundo real incluyen una implementación de heapsort como alternativa en caso de que detecten que quicksort se está volviendo degenerado. Heapsort es un algoritmo in situ , pero no es un algoritmo de ordenación estable .
El algoritmo Heapsort fue inventado por JWJ Williams en 1964. [ 4 ] El artículo también introdujo el montón binario como una estructura de datos útil por derecho propio. [ 5 ] Ese mismo año, Robert W. Floyd publicó una versión mejorada que podía ordenar un arreglo in situ, continuando su investigación previa sobre el algoritmo Treesort . [ 5 ]
Descripción general
El algoritmo de ordenación por montículos se puede dividir en dos fases: construcción del montículo y extracción del montículo.
El montón es una estructura de datos implícita que no ocupa espacio más allá del array de objetos a ordenar; el array se interpreta como un árbol binario completo donde cada elemento del array es un nodo y los enlaces padre e hijo de cada nodo se definen mediante aritmética simple sobre los índices del array. Para un array de base cero, el nodo raíz se almacena en el índice 0, y los nodos vinculados al nodo ison
iLeftChild(i) = 2⋅i + 1 iRightChild(i) = 2⋅i + 2 iParent(i) = floor((i−1) / 2)
donde la función piso redondea hacia abajo al entero anterior. Para una explicación más detallada, consulte Montículo binario § Implementación de montículo .
Este árbol binario es un montículo máximo cuando cada nodo es mayor o igual que sus dos hijos. De forma equivalente, cada nodo es menor o igual que su padre. Esta regla, aplicada a lo largo de todo el árbol, hace que el nodo máximo se encuentre en la raíz.
En la primera fase, se construye un montón a partir de los datos (véase Montón binario § Construcción de un montón ).
En la segunda fase, el montón se convierte en un arreglo ordenado eliminando repetidamente el elemento más grande (la raíz del montón) y colocándolo al inicio del arreglo. El montón se actualiza después de cada eliminación para mantener su estructura. Una vez que se han eliminado todos los objetos del montón, el resultado es un arreglo ordenado.
El algoritmo Heapsort se realiza normalmente in situ. En la primera fase, el array se divide en un prefijo sin ordenar y un sufijo ordenado por montículo (inicialmente vacío). En cada paso, el prefijo se reduce y el sufijo se expande. Cuando el prefijo está vacío, esta fase finaliza. En la segunda fase, el array se divide en un prefijo ordenado por montículo y un sufijo ordenado (inicialmente vacío). En cada paso, el prefijo se reduce y el sufijo se expande. Cuando el prefijo está vacío, el array está ordenado.
Algoritmo
El algoritmo de ordenación por montículos comienza reorganizando el arreglo en un montículo binario máximo. A continuación, el algoritmo intercambia repetidamente la raíz del montículo (el elemento más grande que queda) con su último elemento, que luego se declara como parte del sufijo ordenado. Posteriormente, el montículo, que se vio afectado por el cambio de la raíz, se repara para que el elemento más grande vuelva a estar en la raíz. Este proceso se repite hasta que solo queda un valor en el montículo.
Los pasos son:
- Llama a la
heapify()función en el array. Esto crea un montón a partir de un array en O ( n ) operaciones. - Intercambia el primer elemento del arreglo (el elemento más grande en el montón) con el último elemento del montón. Disminuye el rango considerado del montón en uno.
- Llama a la
siftDown()función en el array para mover el nuevo primer elemento a su lugar correcto en el montón. - Vuelva al paso (2) hasta que el array restante sea un solo elemento.
La heapify()operación se ejecuta una vez y su rendimiento es O ( n )siftDown() . La función se llama n veces y requiere O (log n ) de trabajo cada vez, debido a que su recorrido comienza desde el nodo raíz. Por lo tanto, el rendimiento de este algoritmo es O ( n + n log n ) = O ( n log n ) .
El núcleo del algoritmo es la siftDown()función. Esta construye montones binarios a partir de montones más pequeños, y puede entenderse de dos maneras equivalentes:
- Dados dos montones binarios y un nodo padre compartido que no forma parte de ninguno de los dos montones, fusionarlos en un único montón binario más grande; o
- Dado un montón binario "dañado", donde la propiedad max-heap (ningún hijo es mayor que su padre) se cumple en todas partes excepto posiblemente entre el nodo raíz y sus hijos, repárelo para producir un montón intacto.
Para establecer la propiedad de montículo máximo en la raíz, se deben comparar hasta tres nodos (la raíz y sus dos hijos), y el mayor debe convertirse en la raíz. Esto se hace más fácilmente encontrando el hijo mayor y luego comparándolo con la raíz. Hay tres casos:
- Si no hay hijos (los dos montones originales están vacíos), la propiedad del montón se cumple trivialmente y no se requiere ninguna otra acción.
- Si la raíz es mayor o igual que el hijo mayor, se cumple la propiedad del montón y, por lo tanto, no se requiere ninguna otra acción.
- Si la raíz es menor que el hijo mayor, intercambie los dos nodos. La propiedad de montón se cumple ahora en el nodo recién promovido (es mayor o igual que sus dos hijos, e incluso mayor que cualquier descendiente), pero puede incumplirse entre la antigua raíz degradada y sus nuevos hijos. Para corregir esto, repita la
siftDown()operación en el subárbol cuya raíz es la antigua raíz degradada.
El número de iteraciones en cualquier siftdown()llamada está limitado por la altura del árbol, que es ⌊ log 2 n ⌋ = O (log n ) .
Pseudocódigo
A continuación se muestra una forma sencilla de implementar el algoritmo en pseudocódigo . Los arreglos están indexados desde cero y swapse utiliza para intercambiar dos elementos del arreglo. El movimiento "hacia abajo" significa desde la raíz hacia las hojas, o desde índices más bajos a más altos. Nótese que durante la ordenación, el elemento más grande se encuentra en la raíz del montón en a[0], mientras que al final de la ordenación, el elemento más grande se encuentra en a[end].
El procedimiento heapsort(a, count) recibe como entrada: un arreglo no ordenado a de longitud count.(Construye el montón en el array a de manera que el valor más grande esté en la raíz) amontonar(a, count) (El siguiente bucle mantiene las invariantes de que a[0:end−1] es un montón, y cada elemento a[end:count−1] más allá de end es mayor que todo lo que lo precede, es decir, a[end:count−1] está ordenado.) fin ← recuento mientras end > 1 hacer (el tamaño del montón se reduce en uno) fin ← fin − 1 (a[0] es la raíz y el valor más grande. El intercambio lo mueve delante de los elementos ordenados.) intercambio(a[fin], a[0]) (el intercambio arruinó la propiedad del montón, así que restáurela) siftDown(a, 0, end)
La rutina de ordenación utiliza dos subrutinas, heapifyy siftDown. La primera es la rutina común de construcción de montón in situ, mientras que la segunda es una subrutina común para implementar heapify.
(Coloca los elementos de 'a' en orden de montón, in situ) procedimiento heapify(a, count) es (start se inicializa en el primer nodo hoja) (el último elemento en un array basado en 0 está en el índice count-1; encuentra el padre de ese elemento) inicio ← iParent(count-1) + 1 mientras inicio > 0 hacer (ir al último nodo que no sea de montón) inicio ← inicio − 1 (desplazar el nodo en el índice 'start' al lugar adecuado de manera que todos los nodos por debajo del índice 'start' estén en orden de montón) siftDown(a, inicio, conteo) (Después de filtrar la raíz, todos los nodos/elementos están en orden de montón)(Reparar el montón cuyo elemento raíz está en el índice 'start', suponiendo que los montones con raíz en sus hijos son válidos) procedimiento siftDown(a, raíz, fin) es mientras iLeftChild(raíz) < fin hacer (Mientras la raíz tenga al menos un hijo) hijo ← iLeftChild(raíz) (Hijo izquierdo de la raíz) (Si hay un hijo derecho y ese hijo es mayor) si hijo+1 < fin y a[hijo] < a[hijo+1] entonces niño ← niño + 1 si a[raíz] < a[hijo] entonces intercambio(a[raíz], a[hijo]) raíz ← hijo (repetir para continuar filtrando hacia abajo el hijo ahora) de lo contrario (La raíz contiene el elemento más grande. Dado que podemos asumir que los montones con raíz en los hijos son válidos, esto significa que hemos terminado.) retornar
El heapifyprocedimiento consiste en construir pequeños montículos y fusionarlos repetidamente siftDown. Comienza con las hojas, observando que son montículos triviales pero válidos por sí mismos, y luego agrega los padres. A partir del elemento n /2 y retrocediendo, cada nodo interno se convierte en la raíz de un montículo válido mediante un proceso de filtrado descendente. El último paso consiste en filtrar el primer elemento, tras lo cual todo el arreglo cumple con la propiedad de montículo.
Para comprobar que esto requiere un tiempo O ( n ) , cuente el número de iteraciones en el peor de los casos siftDown. La última mitad del arreglo requiere cero iteraciones, el cuarto anterior requiere como máximo una iteración, el octavo anterior requiere como máximo dos iteraciones, el decimosexto anterior requiere como máximo tres, y así sucesivamente.
Visto de otra manera, si asumimos que cada siftDownllamada requiere el número máximo de iteraciones, la primera mitad del array requiere una iteración, el primer cuarto requiere una más (un total de 2), el primer octavo requiere otra más (un total de 3), y así sucesivamente.
Esto suma n /2 + n /4 + n /8 + ⋯ = n⋅(1/2 + 1/4 + 1/8 + ⋯) , donde la suma infinita es una serie geométrica bien conocida cuya suma es 1 , por lo tanto el producto es simplemente n .
Lo anterior es una aproximación. Se sabe que el número exacto de comparaciones en el peor de los casos durante la fase de construcción del montón de heapsort es igual a 2 n − 2 s 2 ( n ) − e 2 ( n ) , donde s 2 ( n ) es el número de bits 1 en la representación binaria de n y e 2 ( n ) es el número de bits 0 finales . [ 6 ] [ 7 ]
Implementación estándar
Aunque es conveniente pensar en las dos fases por separado, la mayoría de las implementaciones las combinan, lo que permite que una sola instancia de siftDownse expanda en línea . [ 8 ] : Algoritmo H Dos variables (aquí, starty end) mantienen un registro de los límites del área del montón. La porción del arreglo antes startde no está ordenada, mientras que la porción que comienza en endestá ordenada. La construcción del montón disminuye starthasta que es cero, después de lo cual la extracción del montón disminuye endhasta que es 1 y el arreglo está completamente ordenado.
El procedimiento heapsort(a, count) recibe como entrada: un arreglo no ordenado a de longitud count. inicio ← piso(conteo/2) fin ← recuento mientras fin > 1 hacer si inicio > 0 entonces (construcción de montón) inicio ← inicio − 1 de lo contrario (extracción de montón) fin ← fin − 1 intercambio(a[fin], a[0]) (Lo siguiente es siftDown(a, inicio, fin)) raíz ← inicio mientras iLeftChild(root) < fin hacer hijo ← iLeftChild(raíz) (Si hay un hijo derecho y ese hijo es mayor) si hijo+1 < fin y a[hijo] < a[hijo+1] entonces niño ← niño + 1 si a[raíz] < a[hijo] entonces intercambio(a[raíz], a[hijo]) raíz ← hijo (repetir para seguir filtrando hacia abajo el hijo ahora) de lo contrario salir (volver al bucle exterior)
Variaciones
Construcción de montones de Williams
La descripción anterior utiliza el algoritmo de construcción de montículos mejorado de Floyd, que opera en tiempo O ( n ) y utiliza la misma siftDownprimitiva que la fase de extracción del montículo. Si bien este algoritmo, al ser más rápido y sencillo de programar, es utilizado por todas las implementaciones prácticas de ordenación por montículos, el algoritmo original de Williams puede ser más fácil de entender y es necesario para implementar una cola de prioridad de montículos binarios más general .
En lugar de fusionar muchos montículos pequeños, el algoritmo de Williams mantiene un único montículo al principio del array y añade repetidamente un elemento adicional mediante una siftUpprimitiva. Al estar al final del array, el nuevo elemento es una hoja y no tiene hijos de los que preocuparse, pero puede violar la propiedad de montículo al ser mayor que su padre. En este caso, se intercambia con su padre y se repite la prueba hasta que el padre sea mayor o no haya padre (se ha llegado a la raíz). En pseudocódigo, esto es:
El procedimiento siftUp(a, end) es la entrada: a es el array, que está ordenado por montículo hasta end-1. end es el nodo a clasificar. mientras end > 0 padre := iParent(fin) Si a[padre] < a[fin] entonces (fuera del orden de montículo máximo) intercambio(a[padre], a[fin]) fin := padre (continuar filtrando hacia arriba) de lo contrario devolverEl procedimiento heapify(a, count) es (comenzar con un montón trivial de un solo elemento) fin := 1 mientras end < count (desplazar el nodo en el índice end al lugar adecuado de manera que todos los nodos por encima del índice end estén en orden de montón) siftUp(a, fin) fin := fin + 1 (después de filtrar el último nodo, todos los nodos están en orden de montón)

Para entender por qué este algoritmo puede tardar asintóticamente más tiempo en construir un montón ( O ( n log n ) frente a O ( n ) en el peor de los casos), tenga en cuenta que en el algoritmo de Floyd, casi todas las llamadas a siftDownlas operaciones se aplican a montones muy pequeños . La mitad de los montones son montones triviales de altura 1 y pueden omitirse por completo, la mitad de los restantes son de altura 2, y así sucesivamente. Solo dos llamadas se realizan en montones de tamaño n /2 , y solo una siftDownoperación se realiza en el montón completo de nsiftDown elementos. La operación promedio general tarda O (1) tiempo.
En contraste, en el algoritmo de Williams la mayoría de las llamadas siftUpse realizan en grandes montones de altura O (log n ) . La mitad de las llamadas se realizan con un tamaño de montón de n /2 o más, tres cuartas partes se realizan con un tamaño de montón de n /4 o más, y así sucesivamente. Aunque el número promedio de pasos es similar a la técnica de Floyd, [ 9 ] : 3 entradas preordenadas causarán el peor caso: cada nodo agregado se tamiza hasta la raíz, por lo que la llamada promedio siftUprequerirá aproximadamente (log 2 n − 1)/2 + (log 2 n − 2)/4 + (log 2 n − 3)/8 + ⋯ = log 2 n − (1 + 1/2 + 1/4 + ⋯) = log 2 n − 2 iteraciones.
Debido a que está dominado por la segunda fase de extracción de montículos, el algoritmo heapsort en sí tiene una complejidad temporal de O ( n log n ) utilizando cualquiera de las versiones de heapify.
Ordenación por montículos ascendente
El ordenamiento por montículos ascendente es una variante que reduce significativamente el número de comparaciones necesarias. Mientras que el ordenamiento por montículos descendente ordinario requiere 2 n log 2 n + O ( n ) comparaciones en el peor de los casos y en promedio, [ 10 ] la variante ascendente requiere n log 2 n + O (1) comparaciones en promedio, [ 10 ] y 1,5 n log 2 n + O ( n ) en el peor de los casos. [ 11 ]
Si las comparaciones son sencillas (por ejemplo, con claves enteras), la diferencia es irrelevante [ 12 ], ya que el ordenamiento por montículos descendente compara valores que ya se han cargado desde la memoria. Sin embargo, si las comparaciones requieren una llamada a una función u otra lógica compleja, el ordenamiento por montículos ascendente resulta ventajoso.
Esto se logra utilizando un siftDownprocedimiento más elaborado. El cambio mejora ligeramente la fase de construcción del montón en tiempo lineal, [ 13 ] pero es más significativo en la segunda fase. Al igual que en el ordenamiento por montículos descendente, cada iteración de la segunda fase extrae la parte superior del montón, a[0], y llena el espacio que deja con a[end], luego tamiza este último elemento hacia abajo en el montón. Pero este elemento proviene del nivel más bajo del montón, lo que significa que es uno de los elementos más pequeños en el montón, por lo que el tamizado hacia abajo probablemente tomará muchos pasos para moverlo de nuevo hacia abajo. [ 14 ] En el ordenamiento por montículos descendente, cada paso de siftDownrequiere dos comparaciones, para encontrar el mínimo de tres elementos: el nuevo nodo y sus dos hijos.
El algoritmo de ordenación por montículos de abajo hacia arriba conceptualmente reemplaza la raíz con un valor de −∞ y la desplaza hacia abajo utilizando solo una comparación por nivel (ya que ningún hijo puede ser menor que −∞) hasta que se alcanzan las hojas, luego reemplaza el −∞ con el valor correcto y la desplaza hacia arriba (nuevamente, utilizando una comparación por nivel) hasta que se encuentra la posición correcta.
Esto sitúa la raíz en la misma ubicación que en el método descendente siftDown, pero se requieren menos comparaciones para encontrar esa ubicación. Para cualquier siftDownoperación individual, la técnica ascendente es ventajosa si el número de movimientos descendentes es al menos 2/3 de la altura del árbol (cuando el número de comparaciones es 4/3 veces la altura para cualquiera de las técnicas), y resulta que esto es más que cierto en promedio, incluso para las peores entradas. [ 11 ]
Una implementación ingenua de este algoritmo conceptual provocaría una copia de datos redundante, ya que la parte ascendente deshace parte de la descendente. Una implementación práctica busca hacia abajo una hoja donde se colocaría −∞, y luego hacia arriba donde debería colocarse la raíz. Finalmente, el recorrido ascendente continúa hasta la posición inicial de la raíz, sin realizar más comparaciones, sino intercambiando nodos para completar la reorganización necesaria. Esta forma optimizada realiza el mismo número de intercambios que la descendente siftDown.
Debido a que llega hasta el fondo y luego vuelve a subir, algunos autores lo llaman ordenamiento por montículos con rebote . [ 15 ]
función leafSearch(a, i, end) es j ← i mientras iRightChild(j) < fin hacer (Determinar cuál de los dos hijos de j es el mayor) si a[iRightChild(j)] > a[iLeftChild(j)] entonces j ← iRightChild(j) demás j ← iHijoIzquierdo(j) (En el último nivel, puede que solo haya un hijo) si iLeftChild(j) < fin entonces j ← iHijoIzquierdo(j) devolver j
El valor de retorno leafSearchse utiliza en la siftDownrutina modificada: [ 11 ]
El procedimiento siftDown(a, i, end) es j ← leafSearch(a, i, end) mientras a[i] > a[j] hacer j ← iParent(j) mientras j > yo hago intercambio(a[i], a[j]) j ← iParent(j)
Se anunció que el algoritmo de ordenación por montículos de abajo hacia arriba superaba al algoritmo de ordenación rápida (con selección de pivote mediana de tres) en matrices de tamaño ≥16000. [ 10 ]
Una reevaluación de este algoritmo en 2008 demostró que no era más rápido que el ordenamiento por montículos descendente para claves enteras, presumiblemente porque la predicción de ramificaciones moderna anula el costo de las comparaciones predecibles que el ordenamiento por montículos ascendente logra evitar. [ 12 ]
Un refinamiento adicional realiza una búsqueda binaria en la búsqueda ascendente y ordena en el peor caso de ( n +1)(log 2 ( n +1) + log 2 log 2 ( n +1) + 1.82) + O (log 2 n ) comparaciones, aproximándose al límite inferior teórico de la información de n log 2 n − 1.4427 n comparaciones. [ 16 ]
Una variante que utiliza dos bits adicionales por nodo interno ( n −1 bits en total para un montón de n elementos) para almacenar en caché información sobre qué hijo es mayor (se requieren dos bits para almacenar tres casos: izquierda, derecha y desconocido) [ 13 ] utiliza menos de n log 2 n + 1.1 n comparaciones. [ 17 ]
Otras variaciones
- El algoritmo de ordenación por montículos ternario utiliza un montículo ternario en lugar de uno binario; es decir, cada elemento del montículo tiene tres hijos. Es más complejo de programar, pero realiza un número constante de veces menos operaciones de intercambio y comparación. Esto se debe a que cada paso de sift-down en un montículo ternario requiere tres comparaciones y un intercambio, mientras que en un montículo binario se requieren dos comparaciones y un intercambio. Dos niveles en un montículo ternario cubren 3 2 = 9 elementos, realizando más trabajo con seis comparaciones que tres niveles en el montículo binario, que solo cubren 2 3 = 8. Esto es principalmente de interés académico, o como ejercicio para estudiantes, [ 18 ] ya que la complejidad adicional no justifica el pequeño ahorro, y la ordenación por montículos de abajo hacia arriba supera a ambas.
- El algoritmo de ordenación por montículos optimizado para memoria [ 19 ] : 87 mejora la localidad de referencia de la ordenación por montículos al aumentar aún más el número de hijos. Esto incrementa el número de comparaciones, pero debido a que todos los hijos se almacenan consecutivamente en la memoria, reduce el número de líneas de caché a las que se accede durante el recorrido del montículo, lo que supone una mejora neta del rendimiento.
- La implementación estándar del algoritmo de construcción de montículos de Floyd provoca un gran número de fallos de caché una vez que el tamaño de los datos supera el de la caché de la CPU . [ 19 ] : 87 Se puede obtener un mejor rendimiento en conjuntos de datos grandes fusionando en orden de profundidad , combinando submontículos lo antes posible, en lugar de combinar todos los submontículos de un nivel antes de pasar al superior. [ 9 ] [ 20 ]
- El heapsort fuera de lugar [ 21 ] [ 22 ] [ 14 ] mejora el heapsort de abajo hacia arriba al eliminar el peor caso, garantizando n log 2 n + O ( n ) comparaciones. Cuando se toma el máximo, en lugar de llenar el espacio desocupado con un valor de datos no ordenado, se llena con un valor centinela −∞ , que nunca "rebota" hacia arriba. Resulta que esto puede usarse como una primitiva en un algoritmo "QuickHeapsort" in situ (y no recursivo). [ 23 ] Primero, se realiza un paso de partición similar a quicksort, pero invirtiendo el orden de los datos particionados en el arreglo. Supongamos ( sin pérdida de generalidad ) que la partición más pequeña es la mayor que el pivote, que debería ir al final del arreglo, pero nuestro paso de partición invertido la coloca al principio. Crea un montón a partir de la partición más pequeña y realiza una ordenación por montículos fuera de lugar sobre él, intercambiando los máximos extraídos con valores del final del array. Estos son menores que el pivote, es decir, menores que cualquier valor en el montón, por lo que sirven como valores centinela de −∞ . Una vez completada la ordenación por montículos (y movido el pivote justo antes del final del array ahora ordenado), el orden de las particiones se ha invertido y la partición más grande al principio del array se puede ordenar de la misma manera. (Como no hay recursión no de cola , esto también elimina el uso de pila O (log n ) de quicksort ).
- El algoritmo smoothsort [ 24 ] es una variación de heapsort desarrollado por Edsger W. Dijkstra en 1981. Al igual que heapsort, el límite superior de smoothsort es O ( n log n ) . La ventaja de smoothsort es que se aproxima a un tiempo de O ( n ) si la entrada ya está ordenada en cierto grado , mientras que heapsort tiene un promedio de O ( n log n ) independientemente del estado de ordenación inicial. Debido a su complejidad, smoothsort se usa con poca frecuencia.
- Levcopoulos y Petersson [ 25 ] describen una variación de heapsort basada en un montón de árboles cartesianos . Primero, se construye un árbol cartesiano a partir de la entrada en tiempo O ( n ) , y su raíz se coloca en un montón binario de 1 elemento. Luego extraemos repetidamente el mínimo del montón binario, generamos el elemento raíz del árbol y agregamos sus hijos izquierdo y derecho (si los hay), que también son árboles cartesianos, al montón binario. [ 26 ] Como muestran, si la entrada ya está casi ordenada, los árboles cartesianos estarán muy desequilibrados, con pocos nodos que tengan hijos izquierdo y derecho, lo que hace que el montón binario permanezca pequeño y permite que el algoritmo ordene más rápidamente que O ( n log n ) para entradas que ya están casi ordenadas.
- Varias variantes, como el ordenamiento por montículos débil, requieren n log 2 n + O (n) comparaciones en el peor de los casos, cerca del mínimo teórico, utilizando un bit adicional de estado por nodo. Si bien este bit adicional hace que los algoritmos no sean verdaderamente in situ, si se puede encontrar espacio para él dentro del elemento, estos algoritmos son simples y eficientes, [ 9 ] : 40 pero aún más lentos que los montículos binarios si las comparaciones de claves son lo suficientemente baratas (por ejemplo, claves enteras) como para que un factor constante no importe. [ 27 ]
- El algoritmo de ordenación por montículos definitivo de Katajainen no requiere almacenamiento adicional, realiza n log 2 n + O (n) comparaciones y un número similar de movimientos de elementos. [ 28 ] Sin embargo, es aún más complejo y no se justifica a menos que las comparaciones sean muy costosas.
Comparación con otros tipos
Heapsort compite principalmente con quicksort , otro algoritmo de ordenación in situ inestable basado en comparaciones, muy eficiente y de propósito general.
Las principales ventajas de Heapsort son su código simple y no recursivo , sus mínimos requisitos de almacenamiento auxiliar y su rendimiento fiable: sus casos mejor y peor se encuentran dentro de un pequeño factor constante entre sí, y del límite inferior teórico de los algoritmos de ordenación por comparación . Si bien no puede superar O ( n log n ) para entradas preordenadas, tampoco sufre del peor caso de O ( n² ) de quicksort .
Las implementaciones reales de quicksort utilizan diversas heurísticas para evitar el peor caso, pero esto hace que su implementación sea mucho más compleja. Implementaciones como introsort y quicksort con patrón desacreditado [ 29 ] utilizan heapsort como último recurso si detectan un comportamiento degenerado. Por lo tanto, su rendimiento en el peor caso es ligeramente inferior al que se obtendría si se hubiera utilizado heapsort desde el principio.
Las principales desventajas de Heapsort son su escasa localidad de referencia y su naturaleza inherentemente serial; los accesos al árbol implícito están muy dispersos y son en su mayoría aleatorios, y no existe una forma sencilla de convertirlo en un algoritmo paralelo .
Las garantías de rendimiento en el peor de los casos hacen que heapsort sea popular en la computación en tiempo real y en sistemas que se ocupan de entradas elegidas maliciosamente [ 30 ], como el kernel de Linux [ 31 ] . La combinación de una implementación pequeña y un rendimiento "suficientemente bueno" confiable lo hacen popular en sistemas embebidos y, en general, en cualquier aplicación donde la ordenación no sea un cuello de botella de rendimiento . Por ejemplo, heapsort es ideal para ordenar una lista de nombres de archivo para su visualización, pero un sistema de gestión de bases de datos probablemente querría un algoritmo de ordenación más optimizado.
Un quicksort bien implementado suele ser 2–3 veces más rápido que heapsort. [ 19 ] [ 32 ] Aunque quicksort requiere menos comparaciones, este es un factor menor. (Los resultados que afirman el doble de comparaciones miden la versión de arriba hacia abajo; véase § Heapsort de abajo hacia arriba ). La principal ventaja de quicksort es su localidad de referencia mucho mejor: la partición es un escaneo lineal con buena localidad espacial, y la subdivisión recursiva tiene buena localidad temporal. Con un esfuerzo adicional, quicksort también puede implementarse en código prácticamente sin bifurcaciones , [ 33 ] [ 34 ] y se pueden usar múltiples CPU para ordenar subparticiones en paralelo. Por lo tanto, quicksort es preferible cuando el rendimiento adicional justifica el esfuerzo de implementación.
El otro algoritmo de ordenación principal O ( n log n ) es la ordenación por fusión , pero rara vez compite directamente con la ordenación por montículos porque no es in situ. El requisito de la ordenación por fusión de Ω( n ) de espacio adicional (aproximadamente la mitad del tamaño de la entrada ) suele ser prohibitivo, excepto en las situaciones en las que la ordenación por fusión tiene una clara ventaja:
- Cuando se requiere una clasificación estable
- Al aprovechar la entrada (parcialmente) preordenada
- Ordenar listas enlazadas (en cuyo caso la ordenación por fusión requiere un espacio adicional mínimo).
- Ordenación paralela; la ordenación por fusión se paraleliza incluso mejor que la ordenación rápida y puede lograr fácilmente una aceleración casi lineal.
- Ordenación externa ; la ordenación por fusión tiene una excelente localidad de referencia.
Ejemplo
Los ejemplos ordenan los valores { 6, 5, 3, 1, 8, 7, 2, 4 } en orden ascendente utilizando ambos algoritmos de construcción de montículos. Los elementos que se comparan se muestran en negrita . Normalmente hay dos al ordenar hacia arriba y tres al ordenar hacia abajo, aunque puede haber menos al llegar a la parte superior o inferior del árbol.
Construcción de montículos (algoritmo de Williams)

Construcción de montículos (algoritmo de Floyd)
Extracción de montones
Notas
- ↑ Bollobás, B.; Fenner, TI; Frieze, AM (1996). "Sobre el mejor caso de Heapsort" (PDF) . Journal of Algorithms . 20 (11): 205– 217. doi : 10.1006/jagm.1996.0011 .
- ↑ Sedgewick, Robert ; Schaffer, Russel W. (octubre de 1990). El mejor caso de Heapsort (Informe técnico). Universidad de Princeton. TR-293-90.
- ↑ Cormen, Thomas H.; Leiserson, Charles Eric; Rivest, Ronald L.; Stein, Clifford (2022). Introducción a los algoritmos (4.ª ed.). Cambridge, Massachusetts: The MIT Press. pág. 170. ISBN 978-0-262-04630-5.
- ↑ Williams 1964
- 1 2 Brass, Peter (2008). Estructuras de datos avanzadas . Cambridge University Press. pág. 209. ISBN 978-0-521-88037-4.
- ↑ Suchenek, Marek A. (2012). "Análisis elemental pero preciso del peor caso del programa de construcción de montones de Floyd". Fundamenta Informaticae . 120 (1): 75– 92. doi : 10.3233/FI-2012-751 .
- ↑ Suchenek, Marek A. (7 de abril de 2015). "Análisis completo del peor caso de Heapsort con verificación experimental de sus resultados, manuscrito". arXiv : 1504.01459 [ cs.DS ].
- ↑ Knuth 1997
- 1 2 3 Bojesen, Jesper; Katajainen, Jyrki; Spork, Maz (2000). "Estudio de caso de ingeniería de rendimiento: construcción de montículos" (PostScript) . ACM Journal of Experimental Algorithmics . 5 (15): 15–es. CiteSeerX 10.1.1.35.3248 . doi : 10.1145/351827.384257 . S2CID 30995934 . Fuente PDF alternativa .
- 1 2 3 Wegener, Ingo (13 de septiembre de 1993). "Bottom-Up Heapsort, una nueva variante de Heapsort que supera, en promedio, a Quicksort (si n no es muy pequeño)" (PDF) . Theoretical Computer Science . 118 (1): 81– 98. doi : 10.1016/0304-3975(93)90364-y .Aunque se trata de una reimpresión de un trabajo publicado por primera vez en 1990 (en la conferencia Fundamentos Matemáticos de la Informática), la técnica fue publicada por Carlsson en 1987. [ 16 ]
- 1 2 3 Fleischer, Rudolf (febrero de 1994). "Un límite inferior ajustado para el peor caso de Bottom-Up-Heapsort" (PDF) . Algorithmica . 11 (2): 104– 115. doi : 10.1007/bf01182770 . hdl : 11858/00-001M-0000-0014-7B02-C . S2CID 21075180. Archivado del original (PDF) el 14 de junio de 2018. Recuperado el 14 de junio de 2018 . También disponible como Fleischer, Rudolf (abril de 1991). Un límite inferior ajustado para el peor caso de Bottom-Up-Heapsort (PDF) (Informe técnico). MPI-INF . MPI-I-91-104.
- 1 2 Mehlhorn, Kurt ; Sanders, Peter (2008). "Colas de prioridad" (PDF) . Algoritmos y estructuras de datos: La caja de herramientas básica . Springer. pág. 142. ISBN 978-3-540-77977-3.
- 1 2 McDiarmid, CJH; Reed, BA (septiembre de 1989). "Building heaps fast" (PDF) . Journal of Algorithms . 10 (3): 352– 365. doi : 10.1016/0196-6774(89)90033-3 .
- 1 2 MacKay, David JC (diciembre de 2005). "Heapsort, Quicksort y entropía" . Recuperado el 12 de febrero de 2021 .
- ↑ Moret, Bernard ; Shapiro, Henry D. (1991). "8.6 Heapsort". Algorithms from P to NP Volumen 1: Diseño y eficiencia . Benjamin/Cummings. pág. 528. ISBN 0-8053-8008-6A falta de un nombre mejor ,
llamamos a este programa mejorado 'ordenamiento por montículos con rebote ' .
- 1 2 Carlsson, Scante (marzo de 1987). "Una variante de ordenación por montículos con un número casi óptimo de comparaciones" (PDF) . Information Processing Letters . 24 (4): 247– 250. doi : 10.1016/0020-0190(87)90142-6 . S2CID 28135103. Archivado del original (PDF) el 27 de diciembre de 2016.
- ↑ Wegener, Ingo (marzo de 1992). "La complejidad en el peor de los casos de la variante de ordenación por montículos ascendente de McDiarmid y Reed es menor que n log n + 1,1 n " . Information and Computation . 97 (1): 86–96 . doi : 10.1016/0890-5401(92)90005-Z .
- ↑ Tenenbaum, Aaron M.; Augenstein, Moshe J. (1981). «Capítulo 8: Ordenación». Estructuras de datos con Pascal . Prentice-Hall. pág. 405. ISBN 0-13-196501-8.
Escribe una rutina de ordenación similar al heapsort, pero que utilice un montón ternario.
- 1 2 3 LaMarca, Anthony; Ladner, Richard E. (abril de 1999). "La influencia de las cachés en el rendimiento de la ordenación" (PDF) . Journal of Algorithms . 31 (1): 66– 104. CiteSeerX 10.1.1.456.3616 . doi : 10.1006/jagm.1998.0985 . S2CID 206567217 . Véase en particular la figura 9c en la página 98.
- ↑ Chen, Jingsen; Edelkamp, Stefan; Elmasry, Amr; Katajainen, Jyrki (27–31 de agosto de 2012). "Construcción de montículos in situ con comparaciones, movimientos y fallos de caché optimizados" (PDF) . Fundamentos matemáticos de la informática 2012. 37.ª conferencia internacional sobre Fundamentos matemáticos de la informática. Lecture Notes in Computer Science. Vol. 7464. pp. 259–270 . doi : 10.1007/978-3-642-32589-2_25 . ISBN 978-3-642-32588-5. S2CID 1462216 . Archivado del original (PDF) el 29 de diciembre de 2016. Véase en particular la figura 3.
- ↑ Cantone, Domenico; Concotti, Gianluca (1–3 de marzo de 2000). QuickHeapsort, una mezcla eficiente de algoritmos de ordenación clásicos . 4.ª Conferencia Italiana sobre Algoritmos y Complejidad. Lecture Notes in Computer Science. Vol. 1767. Roma. pp. 150–162 . ISBN 3-540-67159-5.
- ↑ Cantone, Domenico; Concotti, Gianluca (agosto de 2002). "QuickHeapsort, una mezcla eficiente de algoritmos de ordenación clásicos" (PDF) . Theoretical Computer Science . 285 (1): 25– 42. doi : 10.1016/S0304-3975(01)00288-2 . Zbl 1016.68042 .
- ↑ Diekert, Volker; Weiß, Armin (agosto de 2016). "QuickHeapsort: Modificaciones y análisis mejorado". Theory of Computing Systems . 59 (2): 209– 230. arXiv : 1209.4214 . doi : 10.1007/s00224-015-9656-y . S2CID 792585 .
- ↑ Dijkstra, Edsger W. Smoothsort: una alternativa a la clasificación in situ (EWD-796a) (PDF) . Archivo EW Dijkstra. Centro de Historia Americana, Universidad de Texas en Austin .( transcripción )
- ↑ Levcopoulos, Christos; Petersson, Ola (1989). «Heapsort—Adapted for Presorted Files». WADS '89: Proceedings of the Workshop on Algorithms and Data Structures . Lecture Notes in Computer Science. Vol. 382. Londres, Reino Unido: Springer-Verlag. pp. 499–509 . doi : 10.1007/3-540-51542-9_41 . ISBN 978-3-540-51542-5.Ordenación por montículos: adaptada para archivos preordenados (Q56049336) .
- ↑ Schwartz, Keith (27 de diciembre de 2010). "CartesianTreeSort.hh" . Archivo de código interesante . Recuperado el 5 de marzo de 2019 .
- ↑ Katajainen, Jyrki (23 de septiembre de 2013). Buscando la mejor cola de prioridad: Lecciones aprendidas . Ingeniería de algoritmos (Seminario 13391). Dagstuhl. pp. 19–20 , 24.
- ↑ Katajainen, Jyrki (2–3 de febrero de 1998). El algoritmo de ordenación por montículos definitivo . Computing: 4.º Simposio Australasiano de Teoría. Comunicaciones Australianas de Ciencias de la Computación . Vol. 20, n.º 3. Perth. págs. 87–96 .
- ↑ Peters, Orson RL (9 de junio de 2021). "Pattern-defeating Quicksort". arXiv : 2106.05123 [ cs.DS ].Peters, Orson RL "pdqsort" . Github . Consultado el 2 de octubre de 2023 .
- ↑ Morris, John (1998). "Comparación de los algoritmos de ordenación rápida y por montículo" . Estructuras de datos y algoritmos (apuntes de clase). Universidad de Australia Occidental . Consultado el 12 de febrero de 2021 .
- ↑ https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/lib/sort.c#n205 Código fuente del kernel de Linux
- ↑ Maus, Arne [en noruego] (14 de mayo de 2014). "Ordenación mediante la generación de la permutación de ordenación y el efecto del almacenamiento en caché en la ordenación" .Véase la figura 1 en la página 6.
- ↑ Edelkamp, Stefan; Weiß, Armin (30 de enero de 2019). "BlockQuicksort: evitar predicciones erróneas de sucursales en Quicksort" (PDF) . Revista de algorítmica experimental . 24 1.4. arXiv : 1604.06697 . doi : 10.1145/3274660 .
- ↑ Aumüller, Martin; Hass, Nikolaj (7–8 de enero de 2019). Simple and Fast BlockQuicksort using Lomuto's Partitioning Scheme . Vigésimo primer taller sobre ingeniería y experimentos de algoritmos (ALENEX). San Diego. arXiv : 1810.12047 . doi : 10.1137/1.9781611975499.2 .
Referencias
- Williams, JWJ (1964). "Algoritmo 232 – Heapsort". Communications of the ACM . 7 (6): 347– 348. doi : 10.1145/512274.512284 .
- Floyd, Robert W. (1964). "Algoritmo 245 – Treesort 3" . Communications of the ACM . 7 (12): 701. doi : 10.1145/355588.365103 . S2CID 52864987 .
- Carlsson, Svante [en sueco] (1987). "Resultados del caso promedio en heapsort". BIT Numerical Mathematics . 27 (1): 2– 17. doi : 10.1007/bf01937350 . S2CID 31450060 .
- Knuth, Donald (1997). «§5.2.3, Ordenación por selección». El arte de la programación informática . Vol. 3: Ordenación y búsqueda (3.ª ed.). Addison-Wesley. pp. 144–155 . ISBN 978-0-201-89685-5.
- Cormen, Thomas H .; Leiserson, Charles E.; Rivest , Ronald L .; Stein, Clifford (2001). Introducción a los algoritmos (2.ª ed.). MIT Press y McGraw-Hill. ISBN 0-262-03293-7.Capítulos 6 y 7 respectivamente: Ordenación por montículos y colas de prioridad
- Un PDF del artículo original de Dijkstra sobre Smoothsort
- Tutorial sobre montículos y ordenación por montículos por David Carlson, St. Vincent College.
Enlaces externos
- Algoritmos de ordenación animados: Ordenación por montículos en Wayback Machine (archivado el 6 de marzo de 2015) – demostración gráfica
- Material didáctico sobre el algoritmo Heapsort de la Universidad de Oldenburg: incluye texto, animaciones y ejercicios interactivos.
- Diccionario de algoritmos y estructuras de datos del NIST: Ordenación por montículos
- El algoritmo Heapsort se implementó en 12 lenguajes. Archivado el 28 de diciembre de 2010 en la Wayback Machine.
- La clasificación: una revisión por Paul Hsieh
- Una presentación de PowerPoint dirigida a educadores que muestra cómo funciona el algoritmo de ordenación por montículos (Heap sort).
- Estructuras de datos abiertas – Sección 11.1.3 – Ordenación por montículos , Pat Morin
- Implementación del algoritmo de ordenación por montículos en C
- Implementación del algoritmo de ordenación por montículos en Python
- Implementación del algoritmo de ordenación por montículos en JavaScript
- Clasificación por comparación
- Montones (estructuras de datos)