Articulo de referencia

Ordenación rápida

O(n^2) (rarely) "},"average-time":{"wt":" O(n\\log n) "},"best-time":{"wt":" O(n\\log n) (simple partition) or O(n) (three-way partition and equal keys)"},"space":{"wt":" O(n) a...

Quicksort es un algoritmo de ordenación eficiente y de propósito general . Fue desarrollado por el científico informático británico Tony Hoare en 1959 [ 1 ] [ 2 ] y publicado en 1961 [ 3 ]. Sigue siendo un algoritmo de ordenación de uso común. En general, es ligeramente más rápido que merge sort y heapsort para datos aleatorios, especialmente en distribuciones grandes [ 4 ] .

Quicksort es un algoritmo de divide y vencerás . Funciona seleccionando un elemento "pivote" del arreglo y dividiendo los demás elementos en dos subarreglos, según sean menores o mayores que el pivote. Por esta razón, a veces se le llama ordenamiento por intercambio de particiones . [ 5 ] Los subarreglos se ordenan luego recursivamente . Esto se puede hacer in situ , requiriendo una pequeña cantidad adicional de memoria para realizar el ordenamiento.

Quicksort es un algoritmo de ordenación por comparación , lo que significa que puede ordenar elementos de cualquier tipo para los que se defina una relación de "menor que" (formalmente, un orden total ). Es un algoritmo basado en comparaciones, ya que los elementos a y b solo se intercambian si su orden relativo se ha obtenido en el cierre transitivo de resultados de comparaciones anteriores. La mayoría de las implementaciones de Quicksort no son estables , lo que significa que el orden relativo de los elementos con el mismo orden no se conserva.

El análisis matemático de quicksort muestra que, en promedio , el algoritmo tardaO(norteregistronorte){\displaystyle O(n\log {n})}comparaciones para ordenar n elementos. En el peor de los casos , haceO(norte2){\displaystyle O(n^{2})}comparaciones.

Historia

El algoritmo quicksort fue desarrollado en 1959 por Tony Hoare mientras era estudiante visitante en la Universidad Estatal de Moscú . En ese momento, Hoare trabajaba en un proyecto de traducción automática para el Laboratorio Nacional de Física . Como parte del proceso de traducción, necesitaba ordenar las palabras en oraciones rusas antes de buscarlas en un diccionario ruso-inglés, que estaba en orden alfabético en cinta magnética . [ 6 ] Después de darse cuenta de que su primera idea, la ordenación por inserción , sería lenta, se le ocurrió una nueva idea. Escribió la parte de partición en Mercury Autocode, pero tuvo problemas para manejar la lista de segmentos sin ordenar. Al regresar a Inglaterra, se le pidió que escribiera código para Shellsort . Hoare mencionó a su jefe que conocía un algoritmo más rápido, y su jefe apostó seis peniques a que no lo conocía. Su jefe finalmente aceptó que había perdido la apuesta. Hoare publicó un artículo sobre su algoritmo, incluyendo un análisis teórico, en The Computer Journal Volumen 5, Número 1, 1962, Páginas 10–16 . Más tarde, Hoare conoció ALGOL y su capacidad para realizar recursión, lo que le permitió publicar una versión mejorada del algoritmo en ALGOL en Communications of the Association for Computing Machinery , la principal revista de ciencias de la computación de la época. [ 3 ] [ 7 ] El código ALGOL se publica en Communications of the ACM (CACM), Volumen 4, Número 7, julio de 1961, pp 321 Algoritmo 63: partición y Algoritmo 64: Quicksort .

Quicksort obtuvo una amplia adopción, apareciendo, por ejemplo, en Unix como la subrutina de ordenación de biblioteca predeterminada. Por lo tanto, prestó su nombre a la subrutina de la biblioteca estándar de C qsort [ 8 ] y en la implementación de referencia de Java .

La tesis doctoral de Robert Sedgewick en 1975 se considera un hito en el estudio de Quicksort, donde resolvió muchos problemas abiertos relacionados con el análisis de varios esquemas de selección de pivote, incluyendo Samplesort , partición adaptativa de Van Emden [ 9 ] , así como la derivación del número esperado de comparaciones e intercambios. [ 8 ] Jon Bentley y Doug McIlroy en 1993 incorporaron varias mejoras para su uso en bibliotecas de programación, incluyendo una técnica para tratar elementos iguales y un esquema de pivote conocido como pseudomediana de nueve, donde una muestra de nueve elementos se divide en grupos de tres y luego se elige la mediana de las tres medianas de los tres grupos. [ 8 ] Bentley describió otro esquema de partición más simple y compacto en su libro Programming Pearls que atribuyó a Nico Lomuto . Más tarde, Bentley escribió que usó la versión de Hoare durante años pero nunca la entendió realmente, pero la versión de Lomuto era lo suficientemente simple como para demostrar que era correcta. [ 10 ] Bentley describió Quicksort como el "código más hermoso que jamás había escrito" en el mismo ensayo. El esquema de partición de Lomuto también fue popularizado por el libro de texto Introducción a los algoritmos, aunque es inferior al esquema de Hoare porque realiza tres veces más intercambios en promedio y se degrada a un tiempo de ejecución de O ( n 2 ) cuando todos los elementos son iguales. [ 11 ] McIlroy produciría además una función AntiQuicksort ( aqsort ) en 1998, que consistentemente lleva incluso su variante de Quicksort de 1993 a un comportamiento cuadrático al producir datos adversarios sobre la marcha. [ 12 ]

Algoritmo

Ejemplo completo de ordenación rápida en un conjunto aleatorio de números. El elemento sombreado es el pivote. Siempre se elige como el último elemento de la partición. Sin embargo, elegir siempre el último elemento de la partición como pivote de esta manera resulta en un rendimiento deficiente ( O ( ) ) en arreglos ya ordenados o arreglos de elementos idénticos. Dado que los sub-arreglos de elementos ordenados o idénticos aparecen con frecuencia hacia el final de un procedimiento de ordenación en un conjunto grande, las versiones del algoritmo de ordenación rápida que eligen el pivote como el elemento central se ejecutan mucho más rápido que el algoritmo descrito en este diagrama en conjuntos grandes de números.

Quicksort es un algoritmo de divide y vencerás para ordenar un arreglo, basado en una rutina de particionamiento; los detalles de este particionamiento pueden variar ligeramente, por lo que Quicksort es en realidad una familia de algoritmos estrechamente relacionados. Aplicado a un rango de al menos dos elementos, el particionamiento produce una división en dos subrangos consecutivos no vacíos, de tal manera que ningún elemento del primer subrango sea mayor que ningún elemento del segundo. Después de aplicar esta partición, Quicksort ordena recursivamente los subrangos, posiblemente después de excluir de ellos un elemento en el punto de división que en ese momento se sabe que ya está en su posición final. Debido a su naturaleza recursiva, Quicksort (al igual que la rutina de particionamiento) debe formularse de manera que pueda invocarse para un rango dentro de un arreglo mayor, incluso si el objetivo final es ordenar un arreglo completo. Los pasos para Quicksort in situ son:

  1. Si el rango tiene menos de dos elementos, regrese inmediatamente ya que no hay nada que hacer.
  2. (Opcional) Si el rango es muy corto, utilice un método de ordenación especial y devuelva el resultado. Por ejemplo, los rangos de dos elementos se pueden ordenar con una sola comparación.
  3. De lo contrario, seleccione un valor, llamado pivote , que se encuentre dentro del rango. (La forma en que se elige el pivote afecta considerablemente el rendimiento, pero no la exactitud).
  4. Dividir el rango: reordenar sus elementos en subrangos, de modo que todos los elementos con valores menores que el pivote se encuentren en el primer subrango, mientras que todos los elementos con valores mayores que el pivote se encuentren en el segundo. Los elementos con valores iguales al pivote pueden ubicarse en cualquiera de los dos subrangos, o en un tercer subrango intermedio. Este subrango central es opcional, pero el algoritmo Quicksort requiere que los dos primeros subrangos sean estrictamente menores que el rango original, lo cual se logra fácilmente colocando al menos un elemento pivote en el medio.
  5. Aplique recursivamente el algoritmo quicksort a los dos subrangos, excluyendo el subrango central que ya se encuentra en su posición final.

Estos son los requisitos básicos para un funcionamiento correcto. Los detalles no especificados anteriormente, incluido el algoritmo de partición, y en particular la selección del pivote, pueden afectar significativamente el rendimiento del algoritmo para algunos arreglos de entrada. Por lo tanto, para analizar la eficiencia de quicksort, es necesario especificar primero estas opciones. Aquí, mencionamos dos métodos de partición específicos.

esquema de partición de Lomuto

Este esquema se atribuye a Nico Lomuto y fue popularizado por Bentley en su libro Programming Pearls [ 13 ] y por Cormen et al. en su libro Introduction to Algorithms [ 14 ] . En la mayoría de las formulaciones, este esquema elige como pivote el último elemento del arreglo. El algoritmo mantiene el índice i mientras recorre el arreglo usando otro índice j de tal manera que los elementos en lo a i-1 (inclusive) son menores que el pivote, y los elementos en i a j (inclusive) son iguales o mayores que el pivote. Como este esquema es más compacto y fácil de entender, se usa frecuentemente en material introductorio, aunque es menos eficiente que el esquema original de Hoare, por ejemplo, cuando todos los elementos son iguales [15]. La complejidad de Quicksort con este esquema se degrada a O(n²) cuando el arreglo ya está ordenado , debido a que la partición es la peor posible. [ 11 ] Se han propuesto varias variantes para mejorar el rendimiento, incluyendo diversas formas de seleccionar el pivote, tratar elementos iguales, usar otros algoritmos de ordenación como la ordenación por inserción para arreglos pequeños, etc. En pseudocódigo , una ordenación rápida que ordena los elementos desde lo hasta hi (inclusive) de un arreglo A se puede expresar como: [ 14 ]

// Ordena (una parte de) un array, lo divide en particiones y luego ordena esas particiones. Algoritmo quicksort(A, lo, hi) es // Asegura que los índices estén en el orden correcto. Si lo >= hi || lo < 0 entonces devolver // Particionar el array y obtener el índice pivote p := partición(A, lo, hi) // Ordenar las dos particiones quicksort(A, lo, p - 1) // Lado izquierdo del pivote quicksort(A, p + 1, hi) // Lado derecho del pivote// Divide el array en dos particiones. Algoritmo partition(A, lo, hi) is pivot := A[hi] // Elige el último elemento como pivote.// Índice pivote temporal yo := lo para j := lo hasta hi - 1 hacer // Si el elemento actual es menor o igual que el pivote si A[j] <= pivote entonces // Intercambiar el elemento actual con el elemento en el índice del pivote temporal intercambiar A[i] con A[j] // Mover el índice del pivote temporal hacia adelante i := i + 1 // Intercambiar el pivote con el último elemento intercambiar A[i] con A[hi] devolver i // el índice del pivote

La ordenación de todo el array se realiza mediante quicksort(A, 0, length(A) - 1) .

Plan de partición de Hoare

Una demostración animada de Quicksort utilizando el esquema de partición de Hoare. Los contornos rojos muestran las posiciones de los punteros izquierdo y derecho ( iy jrespectivamente), los contornos negros muestran las posiciones de los elementos ordenados y el cuadrado negro relleno muestra el valor con el que se está comparando ( pivot).

El esquema de partición original descrito por Tony Hoare utiliza dos punteros (índices del rango) que parten de ambos extremos del arreglo que se está particionando y se mueven uno hacia el otro hasta detectar una inversión: un par de elementos, uno mayor que el pivote en el primer puntero y otro menor que el pivote en el segundo. Si en este punto el primer puntero aún está antes que el segundo, estos elementos están en el orden incorrecto entre sí y se intercambian. [ 16 ] Después de esto, los punteros se mueven hacia adentro y se repite la búsqueda de una inversión. Cuando finalmente los punteros se cruzan (el primero apunta después del segundo), no se realiza ningún intercambio; se encuentra una partición válida, con el punto de división entre los punteros cruzados (cualquier entrada que pueda estar estrictamente entre los punteros cruzados es igual al pivote y puede excluirse de ambos subrangos formados). Con esta formulación, es posible que un subrango resulte ser todo el rango original, lo que impediría que el algoritmo avanzara. Por lo tanto, Hoare estipula que, al final, el subrango que contiene el elemento pivote (que aún se encuentra en su posición original) puede reducirse de tamaño excluyendo dicho pivote, después de (si es necesario) intercambiarlo con el elemento del subrango más cercano a la separación; de este modo, se garantiza la finalización del algoritmo quicksort.

Con respecto a esta descripción original, las implementaciones suelen introducir variaciones menores pero importantes. En particular, el esquema presentado a continuación incluye elementos iguales al pivote entre los candidatos para una inversión (por lo que se utilizan pruebas de "mayor o igual que" y "menor o igual que" en lugar de "mayor que" y "menor que", respectivamente; dado que la formulación utiliza `do ... while` en lugar de `repeat ... until`, lo cual se refleja en el uso de operadores de comparación estrictos ). Si bien no hay razón para intercambiar elementos iguales al pivote, este cambio permite omitir las pruebas en los propios punteros, que de otro modo serían necesarias para garantizar que no se salgan del rango. De hecho, dado que al menos una instancia del valor del pivote está presente en el rango, el primer avance de cualquiera de los punteros no puede sobrepasar esta instancia si se utiliza una prueba inclusiva; una vez realizado el intercambio, estos elementos intercambiados ahora están estrictamente por delante del puntero que los encontró, lo que impide que dicho puntero se salga del rango. (Esto último es cierto independientemente de la prueba utilizada, por lo que sería posible usar la prueba inclusiva solo al buscar la primera inversión. Sin embargo, usar una prueba inclusiva en todo momento también garantiza que se encuentre una división cerca del medio cuando todos los elementos del rango son iguales, lo que proporciona una importante ganancia de eficiencia para ordenar arreglos con muchos elementos iguales). El riesgo de producir una separación sin avance se evita de una manera diferente a la descrita por Hoare. Dicha separación solo puede resultar cuando no se encuentran inversiones, y ambos punteros avanzan al elemento pivote en la primera iteración (entonces se considera que se han cruzado y no se produce ningún intercambio).

En pseudocódigo , [ 14 ]

// Ordena (una parte de) un array, lo divide en particiones y luego ordena esas particiones. El algoritmo quicksort(A, lo, hi) es si lo >= 0 && hi >= 0 && lo < hi entonces p := partición(A, lo, hi) quicksort(A, lo, p) // Nota: ahora se incluye el pivote ordenación rápida(A, p + 1, hi) // Divide el array en dos particiones. Algoritmo partition(A, lo, hi) es // Valor pivote pivot := A[lo] // Elige el primer elemento como pivote// Índice izquierdo i := lo - 1 // Índice derecho j := hola + 1 bucle infinito // Mueve el índice izquierdo a la derecha al menos una vez y mientras el elemento en // el índice izquierdo sea menor que el pivote hacer i := i + 1 mientras A[i] < pivote // Mueve el índice derecho hacia la izquierda al menos una vez y mientras el elemento en // el índice derecho sea mayor que el pivote, haz j := j - 1 mientras A[j] > pivote // Si los índices se cruzan, devuelve si i >= j entonces devuelve j // Intercambia los elementos en los índices izquierdo y derecho. Intercambia A[i] con A[j].

El array completo se ordena mediante quicksort(A, 0, length(A) - 1) .

El esquema de Hoare es más eficiente que el esquema de partición de Lomuto porque realiza tres veces menos intercambios en promedio. Además, como se mencionó, la implementación dada crea una partición equilibrada incluso cuando todos los valores son iguales. [ 11 ] , lo que el esquema de Lomuto no hace. Al igual que el esquema de partición de Lomuto, la partición de Hoare también haría que Quicksort se degradara a O ( n 2 ) para la entrada ya ordenada, si el pivote se eligiera como el primer o el último elemento. Sin embargo, con el elemento medio como pivote, los resultados de datos ordenados con (casi) ningún intercambio en particiones de igual tamaño conducen al mejor caso de comportamiento de Quicksort, es decir O ( n log( n )) . Al igual que otros, la partición de Hoare no produce una ordenación estable. En este esquema, la ubicación final del pivote no necesariamente coincide con el índice devuelto, ya que el pivote y los elementos iguales a él pueden terminar en cualquier lugar dentro de la partición después de un paso de partición, y es posible que no se ordenen hasta que se alcance el caso base de una partición con un solo elemento mediante recursión. Por lo tanto, los siguientes dos segmentos sobre los que recurre el algoritmo principal son (lo..p) (elementos ≤ pivote) y (p+1..hi) (elementos ≥ pivote), a diferencia de (lo..p−1) y (p+1..hi) como en el esquema de Lomuto.

Recursiones subsiguientes (expansión del párrafo anterior)

Ampliemos un poco los siguientes dos segmentos en los que se repite el algoritmo principal. Debido a que usamos comparadores estrictos (>, <) en los bucles "do...while" para evitar quedarnos sin rango, existe la posibilidad de que el pivote se intercambie con otros elementos en la función de partición. Por lo tanto, el índice devuelto en la función de partición no es necesariamente donde se encuentra el pivote real. Consideremos el ejemplo de [5, 2, 3, 1, 0] , siguiendo el esquema, después de la primera partición el arreglo se convierte en [0, 2, 1, 3, 5] , el "índice" devuelto es 2, que es el número 1, cuando el pivote real, el que elegimos para comenzar la partición, era el número 3. Con este ejemplo, vemos cómo es necesario incluir el índice devuelto por la función de partición en nuestras recursiones posteriores. Como resultado, se nos presentan las opciones de recurrir sobre (lo..p) y (p+1..hi) , o sobre (lo..p−1) y (p..hi) . La elección de una u otra opción depende del índice ( i o j ) que devolvamos en la función de partición cuando los índices se cruzan, y de cómo elijamos nuestro pivote en la función de partición ( piso o techo ).

Primero, examinemos la elección de la recursión en (lo..p) y (p+1..hi) , con el ejemplo de ordenar un arreglo donde existen múltiples elementos idénticos [0, 0] . Si el índice i (el índice "posterior") se devuelve después de que los índices se cruzan en la función de partición, el índice 1 se devolvería después de la primera partición. La recursión subsiguiente en (lo..p) sería en (0, 1), que corresponde al mismo arreglo [0, 0] . Se produce una separación no progresiva que causa recursión infinita. Por lo tanto, es obvio que cuando se recurre en (lo..p) y (p+1..hi) , dado que la mitad izquierda de la recursión incluye el índice devuelto, es tarea de la función de partición excluir la "cola" en escenarios no progresivos. Es decir, se debe devolver el índice j (el índice "anterior" cuando los índices se cruzan) en lugar de i. Siguiendo una lógica similar, al considerar el ejemplo de un arreglo ya ordenado [0, 1] , la elección del pivote debe ser "floor" para asegurar que los punteros se detengan en el "primero" en lugar del "segundo" (con "ceiling" como pivote, se devolvería el índice 1 y se incluiría en (lo..p), lo que provocaría una recursión infinita). Por la misma razón, debe evitarse elegir el último elemento como pivote.

La elección de la recursión en (lo..p−1) y (p..hi) sigue la misma lógica que la anterior. Dado que la mitad derecha de la recursión incluye el índice devuelto, la función de partición se encarga de excluir el "cabezal" en escenarios sin avance. El índice i (el índice "posterior" después de que los índices se crucen) en la función de partición debe devolverse, y "techo" debe elegirse como pivote. Los dos matices quedan claros, de nuevo, al considerar los ejemplos de ordenar un arreglo con múltiples elementos idénticos ( [0, 0] ) y un arreglo ya ordenado [0, 1] , respectivamente. Cabe destacar que, con esta versión de la recursión, por la misma razón, debe evitarse elegir el primer elemento como pivote.

Problemas de implementación

Elección del pivote

En las primeras versiones de quicksort, el elemento más a la izquierda de la partición solía elegirse como pivote. Desafortunadamente, esto provoca un comportamiento desfavorable en matrices ya ordenadas, un caso de uso bastante común. [ 17 ] El problema se resolvió fácilmente eligiendo un índice aleatorio para el pivote, el índice central de la partición o (especialmente para particiones más largas) la mediana del primer, el medio y el último elemento de la partición como pivote (como recomendó Sedgewick ). [ 18 ] Esta regla de "mediana de tres" contrarresta el caso de entrada ordenada (o en orden inverso) y proporciona una mejor estimación del pivote óptimo (la mediana real) que seleccionar cualquier elemento individual, cuando no se conoce información sobre el orden de la entrada.

Fragmento de código de mediana de tres para la partición de Lomuto:

medio := ⌊(lo + hi) / 2⌋ si A[medio] < A[lo] Intercambiar A[lo] con A[mid] si A[hi] < A[lo] Intercambiar A[lo] con A[hi] si A[medio] < A[alto] Intercambiar A[medio] con A[agudo] pivote := A[hi]

Primero coloca la mediana A[hi], y luego ese nuevo valor A[hi]se utiliza como pivote, como en el algoritmo básico presentado anteriormente.

Específicamente, el número esperado de comparaciones necesarias para ordenar n elementos (ver §  Análisis del caso promedio ) con selección aleatoria de pivote es 1,386 n log n . El pivoteo de mediana de tres reduce esto a C n , 2 ≈ 1,188 n log n , a costa de un aumento del tres por ciento en el número esperado de intercambios. [ 8 ] Una regla de pivoteo aún más fuerte, para arreglos más grandes, es elegir el noveno , una mediana recursiva de tres (Mo3), definida como [ 8 ]

noveno( a ) = mediana (Mo3(primer 1/3 de a ) , Mo3 ( medio 1/3 de a ) , Mo3 ( final 1/3 de a ) )

La selección del elemento pivote también se complica por la existencia de desbordamiento de enteros . Si los índices límite del subconjunto que se está ordenando son suficientemente grandes, la expresión simple para el índice central, ( lo + hi )/2 , provocará un desbordamiento y dará como resultado un índice pivote inválido. Esto se puede solucionar utilizando, por ejemplo, lo + ( hilo )/2 para indexar el elemento central, aunque esto implica una aritmética más compleja. Problemas similares surgen en otros métodos de selección del elemento pivote.

Elementos repetidos

Con un algoritmo de particionamiento como el esquema de partición de Lomuto descrito anteriormente (incluso uno que elige buenos valores de pivote), quicksort muestra un rendimiento deficiente para entradas que contienen muchos elementos repetidos. El problema es claramente evidente cuando todos los elementos de entrada son iguales: en cada recursión, la partición izquierda está vacía (ningún valor de entrada es menor que el pivote) y la partición derecha solo ha disminuido en un elemento (el pivote se ha eliminado). En consecuencia, el esquema de partición de Lomuto tarda un tiempo cuadrático en ordenar un arreglo de valores iguales. Sin embargo, con un algoritmo de particionamiento como el esquema de partición de Hoare, los elementos repetidos generalmente dan como resultado un mejor particionamiento, y aunque pueden ocurrir intercambios innecesarios de elementos iguales al pivote, el tiempo de ejecución generalmente disminuye a medida que aumenta el número de elementos repetidos (con la caché de memoria reduciendo la sobrecarga de intercambio). En el caso de que todos los elementos sean iguales, el esquema de partición de Hoare intercambia elementos innecesariamente, pero el particionamiento en sí es el mejor caso, como se señaló en la sección de partición de Hoare anterior.

Para resolver el problema del esquema de partición de Lomuto (a veces llamado problema de la bandera nacional holandesa [ 8 ] ), se puede utilizar una rutina de partición alternativa de tiempo lineal que separa los valores en tres grupos: valores menores que el pivote, valores iguales al pivote y valores mayores que el pivote. (Bentley y McIlroy lo llaman "partición gorda" y ya estaba implementado en el algoritmo qsort de la versión 7 de Unix [ 8 ] ) . Los valores iguales al pivote ya están ordenados, por lo que solo las particiones menores que y mayores que necesitan ordenarse recursivamente. En pseudocódigo, el algoritmo quicksort se convierte en:

// Ordena (una parte de) un array, lo divide en particiones y luego ordena esas particiones. El algoritmo quicksort(A, lo, hi) es if lo >= 0 && lo < hi then lt, gt := partition(A, lo, hi) // Múltiples valores de retorno ordenación rápida(A, lo, lt - 1) ordenación rápida(A, gt + 1, hola) // Divide el array en tres particiones algoritmo partition(A, lo, hi) es // Valor pivote pivote := A[(lo + hi) / 2] // Elige el elemento central como pivote (división entera)// Índice menor, igual y mayor lt := lo eq := lo gt := hola // Iterar y comparar todos los elementos con el pivote mientras eq <= gt hacer si A[eq] < pivote entonces // Intercambiar los elementos en los índices iguales y menores intercambiar A[eq] con A[lt] // Incrementar el índice menor lt := lt + 1 // Incrementar el índice igual eq := eq + 1 else if A[eq] > pivot then // Intercambiar los elementos en los índices iguales y mayores intercambiar A[eq] con A[gt] // Disminuir el índice mayor gt := gt - 1 else // si A[eq] = pivote entonces // Incrementar el índice igual eq := eq + 1 // Devuelve los índices menor y mayor return lt, gt

El partitionalgoritmo devuelve los índices del primer ('más a la izquierda') y del último ('más a la derecha') elemento de la partición central. Todos los demás elementos de la partición son iguales al pivote y, por lo tanto, están ordenados. En consecuencia, los elementos de la partición no necesitan incluirse en las llamadas recursivas a quicksort.

El mejor escenario para el algoritmo se da cuando todos los elementos son iguales (o se eligen de un pequeño conjunto de kn elementos). En el caso de que todos los elementos sean iguales, el algoritmo quicksort modificado realizará solo dos llamadas recursivas en subarreglos vacíos y, por lo tanto, finalizará en tiempo lineal (siempre que la partitionsubrutina no tarde más que tiempo lineal).

Optimizaciones

Otras optimizaciones importantes, también sugeridas por Sedgewick y ampliamente utilizadas en la práctica, son: [ 19 ] [ 20 ]

  • Para asegurarnos de que se utilice como máximo un espacio de O (log n ) , primero recurramos en el lado más pequeño de la partición, luego usemos una llamada de cola para recurrir en el otro, o bien actualicemos los parámetros para que ya no incluyan el lado más pequeño ahora ordenado, e iteremos para ordenar el lado más grande.
  • Cuando el número de elementos sea inferior a un umbral determinado (por ejemplo, diez elementos), se recomienda utilizar un algoritmo de ordenación no recursivo, como la ordenación por inserción , que realiza menos intercambios, comparaciones u otras operaciones en matrices tan pequeñas. El umbral ideal variará según los detalles de la implementación específica.
  • Una variante anterior de la optimización previa: cuando el número de elementos es menor que el umbral k , simplemente se detiene; luego, después de que se haya procesado todo el array, se realiza una ordenación por inserción. Detener la recursión prematuramente deja el array ordenado k posiciones, lo que significa que cada elemento está como máximo k posiciones lejos de su posición final ordenada. En este caso, la ordenación por inserción tarda O ( kn ) tiempo en terminar la ordenación, que es lineal si k es una constante. [ 21 ] [ 13 ] : 117 En comparación con la optimización de "muchas ordenaciones pequeñas", esta versión puede ejecutar menos instrucciones, pero hace un uso subóptimo de las memorias caché en las computadoras modernas. [ 22 ]

Paralelización

La formulación de divide y vencerás de Quicksort lo hace apto para la paralelización usando paralelismo de tareas . El paso de partición se realiza mediante el uso de un algoritmo de suma de prefijos paralelo para calcular un índice para cada elemento de la matriz en su sección de la matriz particionada. [ 23 ] [ 24 ] Dado una matriz de tamaño n , el paso de partición realiza O( n ) trabajo en O (log n ) tiempo y requiere O( n ) espacio adicional de trabajo. Después de que la matriz ha sido particionada, las dos particiones se pueden ordenar recursivamente en paralelo. Suponiendo una elección ideal de pivotes, Quicksort paralelo ordena una matriz de tamaño n en O( n log n ) trabajo en O(log 2 n ) tiempo usando O( n ) espacio adicional.

Quicksort presenta algunas desventajas en comparación con otros algoritmos de ordenación, como la ordenación por fusión , lo que dificulta su paralelización eficiente. La profundidad del árbol de divide y vencerás de Quicksort influye directamente en la escalabilidad del algoritmo, y esta profundidad depende en gran medida de la elección del pivote. Además, resulta difícil paralelizar de forma eficiente la etapa de particionamiento in situ. El uso de espacio temporal simplifica la etapa de particionamiento, pero aumenta el consumo de memoria y la sobrecarga constante del algoritmo.

Otros algoritmos de ordenación paralela más sofisticados pueden lograr límites de tiempo aún mejores. [ 25 ] Por ejemplo, en 1991 David MW Powers describió un quicksort paralelizado (y un radix sort relacionado ) que puede operar en tiempo O (log n ) en una PRAM ( máquina de acceso aleatorio paralela ) CRCW (lectura y escritura concurrentes ) con n procesadores mediante la partición implícita. [ 26 ]

Análisis formal

Análisis del peor escenario

La partición más desequilibrada ocurre cuando una de las sublistas devueltas por la rutina de partición tiene un tamaño de n − 1. [ 27 ] Esto puede ocurrir si el pivote resulta ser el elemento más pequeño o más grande de la lista, o en algunas implementaciones (por ejemplo, el esquema de partición de Lomuto descrito anteriormente) cuando todos los elementos son iguales.

Si esto ocurre repetidamente en cada partición, entonces cada llamada recursiva procesa una lista de tamaño uno menos que la lista anterior. En consecuencia, se necesitan n − 1 llamadas anidadas antes de llegar a una lista de tamaño 1. Esto significa que el árbol de llamadas es una cadena lineal de n − 1 llamadas anidadas. La i -ésima llamada realiza O ( ni ) trabajo para hacer la partición, yi=0norte(nortei)=O(norte2){\displaystyle \textstyle \sum _ {i=0}^{n}(ni)=O(n^{2})}, por lo que en ese caso quicksort toma un tiempo de O ( n 2 ) .

Análisis del mejor escenario

En el caso más equilibrado, cada partición divide la lista en dos partes casi iguales. Esto significa que cada llamada recursiva procesa una lista de la mitad del tamaño. Por consiguiente, solo se pueden realizar log₂n llamadas anidadas antes de alcanzar una lista de tamaño 1. Esto significa que la profundidad del árbol de llamadas es log₂n . Pero no hay dos llamadas en el mismo nivel del árbol de llamadas que procesen la misma parte de la lista original; por lo tanto, cada nivel de llamadas necesita solo O ( n ) tiempo en total (cada llamada tiene una sobrecarga constante, pero como solo hay O ( n ) llamadas en cada nivel, esto se incluye en el factor O ( n ) ). El resultado es que el algoritmo utiliza solo O ( n log n ) tiempo.

Análisis del caso promedio

Para ordenar un arreglo de n elementos distintos, quicksort toma un tiempo esperado de O ( n log n ) , promediado sobre todas las n ! permutaciones de n elementos con igual probabilidad . Alternativamente, si el algoritmo selecciona el pivote uniformemente al azar del arreglo de entrada, se puede usar el mismo análisis para acotar el tiempo de ejecución esperado para cualquier secuencia de entrada; la esperanza se toma entonces sobre las elecciones aleatorias hechas por el algoritmo (Cormen et al. , Introducción a los algoritmos , [ 14 ] Sección 7.3).

Tres pruebas comunes de esta afirmación utilizan percentiles, recurrencias y árboles de búsqueda binaria, cada una de las cuales proporciona diferentes perspectivas sobre el funcionamiento del algoritmo quicksort.

Utilizando percentiles

Si cada pivote tiene un rango en algún lugar del 50 por ciento medio, es decir, entre el percentil 25 y el percentil 75, entonces divide los elementos con al menos un 25 % y como máximo un 75 % en cada lado. Elegir consistentemente tales pivotes solo tendría que dividir la lista como máximoregistro4/3norte{\displaystyle \log _{4/3}n}veces antes de alcanzar listas de tamaño 1, lo que produce un algoritmo O ( n log n ) .

Cuando la entrada es una permutación aleatoria , el pivote tiene un rango aleatorio, por lo que no se garantiza que esté en el 50% central. Sin embargo, al partir de una permutación aleatoria, el pivote de cada llamada recursiva tiene un rango aleatorio en su lista y, por lo tanto, está en el 50% central aproximadamente la mitad de las veces. Imaginemos que se lanza una moneda: cara significa que el rango del pivote está en el 50% central, cruz significa que no lo está. Ahora imaginemos que la moneda se lanza una y otra vez hasta obtener k caras. Aunque esto podría llevar mucho tiempo, en promedio solo se requieren 2k lanzamientos , y la probabilidad de que la moneda no obtenga k caras después de 100k lanzamientos es altamente improbable (esto se puede hacer riguroso usando límites de Chernoff ). Por el mismo argumento, la recursión de Quicksort terminará en promedio en una profundidad de llamada de solo2registro4/3norte{\displaystyle 2\log _{4/3}n}Pero si su profundidad de llamada promedio es O (log n ) y cada nivel del árbol de llamadas procesa como máximo n elementos, la cantidad total de trabajo realizado en promedio es el producto, O ( n log n ) . El algoritmo no tiene que verificar que el pivote esté en la mitad central siempre que sea un número consistente de veces.

Utilizando argumentos más cuidadosos, es posible extender esta demostración a la versión de Quicksort donde el pivote se elige aleatoriamente, para mostrar una cota de tiempo que se cumple con alta probabilidad : específicamente, para cualquier dadoa4{\displaystyle a\geq 4}, dejardo=(a4)/2{\displaystyle c=(a-4)/2}, entonces con probabilidad al menos11nortedo{\displaystyle 1-{\frac {1}{n^{c}}}}, el número de comparaciones no será mayor2anorteregistro4/3norte{\displaystyle 2an\log _{4/3}n}. [ 28 ]

Utilizando recurrencias

Un enfoque alternativo es establecer una relación de recurrencia para el factor T ( n ) , el tiempo necesario para ordenar una lista de tamaño n . En el caso más desequilibrado, una sola llamada a quicksort implica un trabajo de O ( n ) más dos llamadas recursivas en listas de tamaño 0 y n -1 , por lo que la relación de recurrencia es

T(norte)=O(norte)+T(0)+T(norte1)=O(norte)+T(norte1).{\displaystyle T(n)=O(n)+T(0)+T(n-1)=O(n)+T(n-1).}

Esta es la misma relación que para la ordenación por inserción y la ordenación por selección , y se resuelve en el peor caso T ( n ) = O ( n 2 ) .

En el caso más equilibrado, una sola llamada a quicksort implica un trabajo de O ( n ) más dos llamadas recursivas en listas de tamaño n /2 , por lo que la relación de recurrencia es

T(norte)=O(norte)+2T(norte2).{\displaystyle T(n)=O(n)+2T\left({\frac {n}{2}}\right).}

El teorema maestro para las recurrencias de divide y vencerás nos dice que T ( n ) = O ( n log n ) .

A continuación se presenta el esquema de una demostración formal de la complejidad temporal esperada O ( n log n ) . Supongamos que no hay duplicados, ya que estos podrían manejarse con un preprocesamiento y postprocesamiento de tiempo lineal, o considerarse casos más sencillos que los analizados. Cuando la entrada es una permutación aleatoria, el rango del pivote es uniformemente aleatorio de 0 a n − 1. Entonces, las partes resultantes de la partición tienen tamaños i y ni − 1 , e i es uniformemente aleatorio de 0 a n − 1. Por lo tanto, promediando sobre todas las posibles divisiones y observando que el número de comparaciones para la partición es n − 1 , el número promedio de comparaciones sobre todas las permutaciones de la secuencia de entrada se puede estimar con precisión resolviendo la relación de recurrencia:

do(norte)=norte1+1nortei=0norte1(do(i)+do(nortei1))=norte1+2nortei=0norte1do(i){\displaystyle C(n)=n-1+{\frac {1}{n}}\sum _{i=0}^{n-1}(C(i)+C(n-i-1))=n-1+{\frac {2}{n}}\sum _{i=0}^{n-1}C(i)}
nortedo(norte)=norte(norte1)+2i=0norte1do(i){\displaystyle nC(n)=n(n-1)+2\sum _{i=0}^{n-1}C(i)}
nortedo(norte)(norte1)do(norte1)=norte(norte1)(norte1)(norte2)+2do(norte1){\displaystyle nC(n)-(n-1)C(n-1)=n(n-1)-(n-1)(n-2)+2C(n-1)}
nortedo(norte)=(norte+1)do(norte1)+2norte2{\displaystyle nC(n)=(n+1)C(n-1)+2n-2}
do(norte)norte+1=do(norte1)norte+2norte+12norte(norte+1)do(norte1)norte+2norte+1=do(norte2)norte1+2norte2(norte1)norte+2norte+1do(norte2)norte1+2norte+2norte+1  =do(1)2+i=2norte2i+12i=1norte11i21norte1incógnitadincógnita=2lnnorte{\displaystyle {\begin{aligned}{\frac {C(n)}{n+1}}&={\frac {C(n-1)}{n}}+{\frac {2}{n+1}}-{\frac {2}{n(n+1)}}\leq {\frac {C(n-1)}{n}}+{\frac {2}{n+1}}\\&={\frac {C(n-2)}{n-1}}+{\frac {2}{n}}-{\frac {2}{(n-1)n}}+{\frac {2}{n+1}}\leq {\frac {C(n-2)}{n-1}}+{\frac {2}{n}}+{\frac {2}{n+1}}\\&\ \ \vdots \\&={\frac {C(1)}{2}}+\sum _{i=2}^{n}{\frac {2}{i+1}}\leq 2\sum _{i=1}^{n-1}{\frac {1}{i}}\approx 2\int _{1}^{n}{\frac {1}{x}}\mathrm {d} x=2\ln n\end{aligned}}}

Resolviendo la recurrencia se obtiene C ( n ) = 2 n ln n ≈ 1,39 n log 2 n .

Esto significa que, en promedio, quicksort tiene un rendimiento solo un 39 % inferior al de su mejor caso. En este sentido, se acerca más al mejor caso que al peor. Un algoritmo de ordenación por comparación no puede utilizar menos de log₂ ( n ! ) comparaciones en promedio para ordenar n elementos (como se explica en el artículo "Ordenación por comparación ") y, en el caso de n grande , la aproximación de Stirling da como resultado log₂ ( n !) ≈ n (log₂n log₂e ) , por lo que quicksort no es mucho peor que un algoritmo de ordenación por comparación ideal. Este rápido tiempo de ejecución promedio es otra razón del dominio práctico de quicksort sobre otros algoritmos de ordenación .

Utilizando un árbol de búsqueda binaria

El siguiente árbol de búsqueda binaria (BST) corresponde a cada ejecución de quicksort: el pivote inicial es el nodo raíz; el pivote de la mitad izquierda es la raíz del subárbol izquierdo, el pivote de la mitad derecha es la raíz del subárbol derecho, y así sucesivamente. El número de comparaciones de la ejecución de quicksort es igual al número de comparaciones durante la construcción del BST mediante una secuencia de inserciones. Por lo tanto, el número promedio de comparaciones para quicksort aleatorio es igual al costo promedio de construir un BST cuando los valores insertados(incógnita1,incógnita2,,incógnitanorte){\displaystyle (x_{1},x_{2},\ldots ,x_{n})}formen una permutación aleatoria.

Consideremos un BST creado mediante la inserción de una secuencia(incógnita1,incógnita2,,incógnitanorte){\displaystyle (x_{1},x_{2},\ldots ,x_{n})}de valores que forman una permutación aleatoria. Sea C el costo de creación del BST. Tenemosdo=ij<idoi,j{\displaystyle C=\sum _{i}\sum _{j<i}c_{i,j}}, dóndedoi,j{\displaystyle c_{i,j}}es una variable aleatoria binaria que expresa si durante la inserción deincógnitai{\displaystyle x_{i}}hubo una comparación conincógnitaj{\displaystyle x_{j}}.

Por linealidad de la esperanza , el valor esperadomi[do]{\displaystyle \operatorname {E} [C]}de C esmi[do]=ij<iPr(doi,j){\displaystyle \operatorname {E} [C]=\sum _{i}\sum _{j<i}\Pr(c_{i,j})}.

Fijar i y j < i . Los valoresincógnita1,incógnita2,,incógnitaj{\displaystyle {x_{1},x_{2},\ldots ,x_{j}}}, una vez ordenados, defina j +1 intervalos. La observación estructural central es queincógnitai{\displaystyle x_{i}}se compara conincógnitaj{\displaystyle x_{j}}en el algoritmo si y solo siincógnitai{\displaystyle x_{i}}cae dentro de uno de los dos intervalos adyacentes aincógnitaj{\displaystyle x_{j}}.

Observe que dado(incógnita1,incógnita2,,incógnitanorte){\displaystyle (x_{1},x_{2},\ldots ,x_{n})}es una permutación aleatoria,(incógnita1,incógnita2,,incógnitaj,incógnitai){\displaystyle (x_{1},x_{2},\ldots ,x_{j},x_{i})}también es una permutación aleatoria, por lo que la probabilidad de queincógnitai{\displaystyle x_{i}}está adyacente aincógnitaj{\displaystyle x_{j}}es exactamente2j+1{\displaystyle {\frac {2}{j+1}}}.

Simplificado como el cálculo breve:

mi[do]=ij<i2j+1=O(iregistroi)=O(norteregistronorte).{\displaystyle \operatorname {E} [C]=\sum _{i}\sum _{j<i}{\frac {2}{j+1}}=O\left(\sum _{i}\log i\right)=O(n\log n).}

Complejidad espacial

El espacio utilizado por quicksort depende de la versión utilizada.

La versión in situ de quicksort tiene una complejidad espacial de O (log n ) , incluso en el peor de los casos, cuando se implementa cuidadosamente utilizando las siguientes estrategias.

  • Se utiliza el particionamiento in situ. Este particionamiento inestable requiere un espacio de O (1) .
  • Tras la partición, la partición con menos elementos se ordena primero (recursivamente), requiriendo como máximo un espacio de O (log n ) . A continuación, la otra partición se ordena mediante recursión de cola o iteración, lo que no incrementa la pila de llamadas. Esta idea, como se mencionó anteriormente, fue descrita por R. Sedgewick y mantiene la profundidad de la pila limitada por O (log n ) . [ 18 ] [ 21 ]

El algoritmo Quicksort con particionamiento in situ e inestable utiliza únicamente espacio adicional constante antes de realizar cualquier llamada recursiva. Quicksort debe almacenar una cantidad constante de información para cada llamada recursiva anidada. Dado que en el mejor de los casos realiza como máximo O (log n ) llamadas recursivas anidadas, utiliza un espacio de O (log n ) . Sin embargo, sin el truco de Sedgewick para limitar las llamadas recursivas, en el peor de los casos, Quicksort podría realizar O ( n ) llamadas recursivas anidadas y necesitar O ( n ) espacio auxiliar.

Desde el punto de vista de la complejidad de bits, variables como `lo` y `hi` no utilizan espacio constante; se necesitan O (log n ) bits para indexar una lista de n elementos. Dado que existen tales variables en cada marco de pila, el algoritmo Quicksort, utilizando el truco de Sedgewick, requiere O ((log n ) ² ) bits de espacio. Sin embargo, este requisito de espacio no es demasiado grave, ya que si la lista contuviera elementos distintos, necesitaría al menos O ( n log n ) bits de espacio.

Se han propuesto versiones de Quicksort sin pila. Estas utilizanO(1){\displaystyle O(1)}espacio adicional (más precisamente, una celda del tipo de los registros ordenados, para intercambiar registros, y un número constante de variables enteras utilizadas como índices). [ 29 ]

Otra versión menos común de quicksort, que no utiliza memoria de trabajo, emplea un espacio de O ( n ) y permite implementar una ordenación estable. Esta memoria permite particionar fácilmente el array de entrada de forma estable y luego copiarlo de nuevo al array de entrada para sucesivas llamadas recursivas. La optimización de Sedgewick sigue siendo válida.

Relación con otros algoritmos

Quicksort es una versión optimizada en espacio del algoritmo de ordenación de árboles binarios . En lugar de insertar elementos secuencialmente en un árbol explícito, Quicksort los organiza concurrentemente en un árbol implícito en las llamadas recursivas. Los algoritmos realizan exactamente las mismas comparaciones, pero en un orden diferente. Una propiedad a menudo deseable en un algoritmo de ordenación es la estabilidad; es decir, que el orden de los elementos que se comparan como iguales no se modifica, lo que permite controlar el orden de las tablas de múltiples claves (por ejemplo, listados de directorios o carpetas) de forma natural. Esta propiedad es difícil de mantener para Quicksort in situ (que utiliza solo espacio adicional constante para punteros y búferes, y O (log n ) espacio adicional para la gestión de la recursión explícita o implícita). Para variantes de Quicksort que implican memoria adicional debido a representaciones que utilizan punteros (por ejemplo, listas o árboles) o archivos (efectivamente listas), es trivial mantener la estabilidad. Las estructuras de datos más complejas, o limitadas al disco, tienden a aumentar el coste de tiempo, en general, haciendo un uso cada vez mayor de la memoria virtual o del disco.

El competidor más directo de quicksort es heapsort . Heapsort tiene las ventajas de simplicidad y un tiempo de ejecución en el peor de los casos de O ( n log n ) , pero su tiempo de ejecución promedio suele considerarse más lento que el de quicksort in situ, principalmente debido a su peor localidad de referencia . [ 30 ] Este resultado es discutible; algunas publicaciones indican lo contrario. [ 31 ] [ 32 ] La principal desventaja de quicksort es la complejidad de implementación requerida para evitar malas elecciones de pivote y el rendimiento resultante de O () . Introsort es una variante de quicksort que resuelve este problema cambiando a heapsort cuando se detecta un caso malo. Los principales lenguajes de programación, como C++ (en las implementaciones GNU y LLVM), utilizan introsort. [ 33 ]

Quicksort also competes with merge sort, another O(n log n) sorting algorithm. Merge sort's main advantages are that it is a stable sort and has excellent worst-case performance. The main disadvantage of merge sort is that it is an out-of-place algorithm, so when operating on arrays, efficient implementations require O(n) auxiliary space (vs. O(log n) for quicksort with in-place partitioning and tail recursion, or O(1) for heapsort).

Merge sort works very well on linked lists, requiring only a small, constant amount of auxiliary storage. Although quicksort can be implemented as a stable sort using linked lists, there is no reason to; it will often suffer from poor pivot choices without random access, and is essentially always inferior to merge sort. Merge sort is also the algorithm of choice for external sorting of very large data sets stored on slow-to-access media such as disk storage or network-attached storage.

Bucket sort with two buckets is very similar to quicksort; the pivot in this case is effectively the value in the middle of the value range, which does well on average for uniformly distributed inputs.

Selection-based pivoting

A selection algorithm chooses the kth smallest of a list of numbers; this is an easier problem in general than sorting. One simple but effective selection algorithm works nearly in the same manner as quicksort, and is accordingly known as quickselect. The difference is that instead of making recursive calls on both sublists, it only makes a single tail-recursive call on the sublist that contains the desired element. This change lowers the average complexity to linear or O(n) time, which is optimal for selection, but the selection algorithm is still O(n2) in the worst case.

Una variante de quickselect, el algoritmo de mediana de medianas , elige los pivotes con mayor precisión, asegurándose de que se encuentren cerca del centro de los datos (entre los percentiles 30 y 70), lo que garantiza un tiempo lineal de O ( n ) . Esta misma estrategia de pivotes puede utilizarse para construir una variante de quicksort (quicksort de mediana de medianas) con un tiempo de O ( n log n ) . Sin embargo, la sobrecarga que supone elegir el pivote es significativa, por lo que generalmente no se utiliza en la práctica.

En términos más abstractos, dado un algoritmo de selección O ( n ) , se puede utilizar para encontrar el pivote ideal (la mediana) en cada paso del algoritmo Quicksort y, por lo tanto, generar un algoritmo de ordenación con un tiempo de ejecución de O ( n log n ) . Las implementaciones prácticas de esta variante son considerablemente más lentas en promedio, pero resultan de interés teórico porque demuestran que un algoritmo de selección óptimo puede generar un algoritmo de ordenación óptimo.

Variantes

Ordenación rápida multipivote

En lugar de particionar en dos submatrices usando un solo pivote, el quicksort multipivote (también multiquicksort [ 22 ] ) particiona su entrada en un número s de submatrices usando s − 1 pivotes. Si bien el caso de doble pivote ( s = 3 ) fue considerado por Sedgewick y otros ya a mediados de la década de 1970, los algoritmos resultantes no fueron más rápidos en la práctica que el quicksort "clásico". [ 34 ] Una evaluación de 1999 de un multiquicksort con un número variable de pivotes, ajustado para hacer un uso eficiente de las cachés del procesador, encontró que aumentaba el número de instrucciones en un 20%, pero los resultados de la simulación sugirieron que sería más eficiente en entradas muy grandes. [ 22 ] Una versión de quicksort de doble pivote desarrollada por Yaroslavskiy en 2009 [ 35 ] resultó ser lo suficientemente rápida [ 36 ] como para justificar su implementación en Java 7 , como algoritmo estándar para ordenar arreglos de primitivos (la ordenación de arreglos de objetos se realiza utilizando Timsort ). [ 37 ] Posteriormente se descubrió que la ventaja de rendimiento de este algoritmo estaba relacionada principalmente con el rendimiento de la caché, [ 38 ] y los resultados experimentales indican que la variante de tres pivotes puede tener un rendimiento aún mejor en máquinas modernas. [ 39 ] [ 40 ]

Ordenación rápida externa

Para archivos de disco, es posible una ordenación externa basada en particionamiento similar a quicksort. Es más lenta que la ordenación por fusión externa, pero no requiere espacio adicional en disco. Se utilizan 4 búferes, 2 para entrada y 2 para salida.norte={\displaystyle N=}el número de registros en el archivo,B={\displaystyle B=}el número de registros por búfer yMETRO=norte/B={\displaystyle M=N/B=}el número de segmentos de búfer en el archivo. Los datos se leen (y se escriben) desde ambos extremos del archivo hacia adentro.incógnita{\displaystyle X}representan los segmentos que comienzan al principio del archivo, yY{\displaystyle Y}representan segmentos que comienzan al final del archivo. Los datos se leen en elincógnita{\displaystyle X}yY{\displaystyle Y}leer búferes. Se elige un registro pivote y los registros en elincógnita{\displaystyle X}yY{\displaystyle Y}Los búferes distintos del registro pivote se copian alincógnita{\displaystyle X}escribir búfer en orden ascendente yY{\displaystyle Y}escribir búfer en orden descendente según la comparación con el registro pivote. Una vez queincógnita{\displaystyle X}oY{\displaystyle Y}El búfer se llena, se escribe en el archivo y el siguienteincógnita{\displaystyle X}oY{\displaystyle Y}El búfer se lee del archivo. El proceso continúa hasta que se leen todos los segmentos y queda un búfer de escritura. Si ese búfer es unincógnita{\displaystyle X}búfer de escritura, el registro pivote se agrega a él y elincógnita{\displaystyle X}Se escribe en el búfer. Si ese búfer es unY{\displaystyle Y}búfer de escritura, el registro pivote se antepone alY{\displaystyle Y}búfer y elY{\displaystyle Y}Se ha escrito el búfer. Esto constituye un paso de partición del archivo, y ahora el archivo está compuesto por dos subarchivos. Las posiciones de inicio y fin de cada subarchivo se insertan/extraen en una pila independiente o en la pila principal mediante recursión. Para limitar el espacio de la pila aO(registro2(norte)){\displaystyle O(\log _{2}(n))}El subarchivo más pequeño se procesa primero. Para una pila independiente, se insertan los parámetros del subarchivo más grande en la pila y se itera sobre el subarchivo más pequeño. Para la recursión, se procesa primero el subarchivo más pequeño y luego se itera sobre el subarchivo más grande. Una vez que un subarchivo tiene menos o igual a 4 B registros, se ordena in situ mediante ordenación rápida y se escribe. Ese subarchivo ahora está ordenado y en su lugar en el archivo. El proceso continúa hasta que todos los subarchivos estén ordenados y en su lugar. El número promedio de pasadas en el archivo es aproximadamente1+ln(norte+1)(4B){\displaystyle {\frac {1+\ln(N+1)}{(4B)}}}, pero el patrón del peor caso esnorte{\displaystyle N}pases (equivalentes aO(norte2){\displaystyle O(n^{2})}para la ordenación interna del peor caso). [ 41 ]

Ordenación rápida por radix de tres vías

Este algoritmo es una combinación de ordenación por radix y ordenación rápida. Se elige un elemento del array (el pivote) y se considera el primer carácter (clave) de la cadena (multiclave). Los elementos restantes se dividen en tres conjuntos: aquellos cuyo carácter correspondiente es menor que, igual que y mayor que el carácter del pivote. Se ordenan recursivamente las particiones "menor que" y "mayor que" según el mismo carácter. Se ordena recursivamente la partición "igual a" según el siguiente carácter (clave). Dado que la ordenación se realiza mediante bytes o palabras de longitud W bits, el mejor caso es O ( KN ) y el peor caso O (2KN ) o al menos O ( ) como en la ordenación rápida estándar, dado que para claves únicas N < 2K , K es una constante oculta en todos los algoritmos de ordenación por comparación estándar , incluida la ordenación rápida. Este es un tipo de ordenación rápida de tres vías en la que la partición central representa un subconjunto ordenado (trivialmente) de elementos que son exactamente iguales al pivote.

Ordenación radix rápida

También desarrollado por Powers como un algoritmo PRAM paralelo O ( K ) . Este es nuevamente una combinación de ordenación por radix y ordenación rápida, pero la decisión de partición izquierda/derecha de la ordenación rápida se realiza en bits sucesivos de la clave, y por lo tanto es O ( KN ) para N claves de K bits. Todos los algoritmos de ordenación por comparación asumen implícitamente el modelo transdicotómico con K en Θ (log N ) , ya que si K es menor podemos ordenar en tiempo O ( N ) usando una tabla hash o ordenación de enteros . Si K ≫ log N pero los elementos son únicos dentro de O (log N ) bits, los bits restantes no serán examinados ni por la ordenación rápida ni por la ordenación rápida por radix. De no ser así, todos los algoritmos de ordenación por comparación también tendrán la misma sobrecarga de examinar O ( K ) bits relativamente inútiles , pero la ordenación rápida por radix evitará los comportamientos del peor caso O ( ) de la ordenación rápida estándar y la ordenación rápida por radix, y será más rápida incluso en el mejor caso de esos algoritmos de comparación bajo estas condiciones de uniqueprefix( K ) ≫ log N. Consulte Powers [ 42 ] para obtener más información sobre las sobrecargas ocultas en la ordenación por comparación, por radix y en paralelo.

Ordenación rápida por bloques

En cualquier algoritmo de ordenación basado en comparaciones, minimizar el número de comparaciones requiere maximizar la cantidad de información obtenida de cada comparación, lo que significa que los resultados de la comparación son impredecibles. Esto provoca frecuentes predicciones erróneas de bifurcaciones , lo que limita el rendimiento. [ 43 ] BlockQuicksort [ 44 ] reorganiza los cálculos de quicksort para convertir las bifurcaciones impredecibles en dependencias de datos . Al particionar, la entrada se divide en bloques de tamaño moderado (que caben fácilmente en la caché de datos ), y dos matrices se llenan con las posiciones de los elementos a intercambiar. (Para evitar bifurcaciones condicionales, la posición se almacena incondicionalmente al final de la matriz, y el índice del final se incrementa si se necesita un intercambio). Una segunda pasada intercambia los elementos en las posiciones indicadas en las matrices. Ambos bucles tienen solo una bifurcación condicional, una prueba de terminación, que generalmente se toma.

La técnica BlockQuicksort está incorporada en la implementación de la STL de C++ de LLVM , libcxx, lo que proporciona una mejora del 50 % en secuencias de enteros aleatorios. El algoritmo quicksort que evita patrones ( pdqsort ), una versión de introsort, también incorpora esta técnica. [ 33 ]

Ordenación rápida parcial e incremental

Existen varias variantes del algoritmo quicksort que separan los k elementos más pequeños o más grandes del resto de la entrada.

Generalización

Richard Cole y David C. Kandathil, en 2004, descubrieron una familia de algoritmos de ordenación de un parámetro, llamados ordenaciones por partición, que en promedio (con todos los ordenamientos de entrada igualmente probables) realizan como máximonorteregistronorte+O(norte){\displaystyle n\log n+{O}(n)}comparaciones (cercanas al límite inferior de la teoría de la información) yΘ(norteregistronorte){\displaystyle {\Theta }(n\log n)}operaciones; en el peor de los casos, realizanΘ(norteregistro2norte){\displaystyle {\Theta }(n\log ^{2}n)}comparaciones (y también operaciones); estas ya están implementadas y solo requieren información adicional.O(registronorte){\displaystyle {O}(\log n)}espacio. Se demostró eficiencia práctica y menor varianza en el rendimiento en comparación con los algoritmos quicksort optimizados (de Sedgewick y Bentley - McIlroy ). [ 45 ]

Véase también

Notas

  1. "Sir Antony Hoare" . Museo de Historia de la Computación. Archivado del original el 3 de abril de 2015. Consultado el 22 de abril de 2015 .
  2. Wilson, John (3 de abril de 2026). "Robert Fox, Mary Rand MBE, Sir Tony Hoare, Biruté Galdikas" . Last Word , Radio 4. Reino Unido: BBC . Consultado el 4 de abril de 2026 .(Minuto 14 y 50 segundos del programa, entrevista con Bill Roscoe ).
  3. ^ Hoare , COCHE (1961). "Algoritmo 64: clasificación rápida". Com. ACM . 4 (7): 321. doi : 10.1145/366622.366644 .
  4. Skiena, Steven S. (2008). Manual de diseño de algoritmos . Springer. pág. 129. ISBN  978-1-84800-069-8.
  5. CL Foster, Algoritmos, abstracción e implementación , 1992, ISBN 0122626605pág. 98
  6. Shustek, L. (2009). "Entrevista: Una entrevista con CAR Hoare". Comm. ACM . 52 (3): 38– 41. doi : 10.1145/1467247.1467261 . S2CID 1868477 . 
  7. "Mi breve entrevista con Sir Tony Hoare, el inventor de Quicksort" . Marcelo M De Barros. 15 de marzo de 2015.
  8. 1 2 3 4 5 6 7 Bentley, Jon L.; McIlroy, M. Douglas (1993). "Ingeniería de una función de ordenación" . Software: Práctica y experiencia . 23 (11): 1249– 1265. CiteSeerX 10.1.1.14.8162 . doi : 10.1002/spe.4380231105 . S2CID 8822797 .  
  9. Van Emden, MH (1 de noviembre de 1970). "Algoritmos 402: Aumentando la eficiencia de Quicksort" . Commun. ACM . 13 (11): 693– 694. doi : 10.1145/362790.362803 . ISSN 0001-0782 . S2CID 4774719 .  
  10. Bentley, Jon (2007). «El código más bello que nunca escribí». En Oram, Andy; Wilson, Greg (eds.). Código bello: Programadores líderes explican cómo piensan . O'Reilly Media. pág. 30. ISBN  978-0-596-51004-6.
  11. 1 2 3 "Particionamiento Quicksort: Hoare vs. Lomuto" . cs.stackexchange.com . Consultado el 3 de agosto de 2015 .
  12. McIlroy, MD (10 de abril de 1999). "Un adversario letal para quicksort" (PDF) . Software: Practice and Experience . 29 (4): 341– 344. doi : 10.1002/(SICI)1097-024X(19990410)29:4 < 341::AID-SPE237 > 3.0.CO ; 2-R . S2CID 35935409 . 
  13. 1 2 Jon Bentley (1999). Perlas de programación . Addison-Wesley Professional.
  14. 1 2 3 4 Cormen, Thomas H .; Leiserson, Charles E .; Rivest, Ronald L .; Stein, Clifford (2009) [1990]. "Clasificación rápida". Introducción a los algoritmos (3ª ed.). MIT Press y McGraw-Hill. págs. 170-190 . ISBN   0-262-03384-4.
  15. Salvaje, Sebastián (2012). Clasificación rápida de doble pivote de Java 7 (tesis). Universidad Técnica de Kaiserslautern.
  16. Hoare, CAR (1 de enero de 1962). "Quicksort" . The Computer Journal . 5 (1): 10– 16. doi : 10.1093/comjnl/5.1.10 . ISSN 0010-4620 . 
  17. Chandramouli, Badrish; Goldstein, Jonathan (18 de junio de 2014). «La paciencia es una virtud» . Actas de la Conferencia Internacional ACM SIGMOD 2014 sobre Gestión de Datos . Sigmod '14. Snowbird, Utah, EE. UU.: ACM. págs. 731–742 . doi : 10.1145/2588555.2593662 . ISBN  978-1-4503-2376-5. S2CID 7830071 . 
  18. 1 2 Sedgewick, Robert (1 de septiembre de 1998). Algoritmos en C: Fundamentos, Estructuras de datos, Ordenación, Búsqueda, Partes 1–4 (3.ª ed.). Pearson Education. ISBN  978-81-317-1291-7.
  19. qsort.c en GNU libc :,
  20. http://www.ugrad.cs.ubc.ca/~cs260/chnotes/ch6/Ch6CovCompiled.html
  21. 1 2 Sedgewick, R. (1978). "Implementación de programas Quicksort". Comm. ACM . 21 (10): 847– 857. doi : 10.1145/359619.359631 . S2CID 10020756 . 
  22. 1 2 3 LaMarca, Anthony; Ladner, Richard E. (1999). "La influencia de las cachés en el rendimiento de la ordenación". Journal of Algorithms . 31 (1): 66– 104. CiteSeerX 10.1.1.27.1788 . doi : 10.1006/jagm.1998.0985 . S2CID 206567217 . Aunque guardar submatrices pequeñas hasta el final tiene sentido desde la perspectiva del recuento de instrucciones, es exactamente lo incorrecto desde la perspectiva del rendimiento de la caché.  
  23. Umut A. Acar, Guy E Blelloch, Margaret Reid-Miller y Kanat Tangwongsan, Quicksort y límites inferiores de ordenación , Estructuras de datos y algoritmos paralelos y secuenciales . 2013.
  24. Breshears, Clay (2012). "Partición Quicksort mediante escaneo de prefijo" . Dr. Dobb's .
  25. Miller, Russ; Boxer, Laurence (2000). Algoritmos secuenciales y paralelos: un enfoque unificado . Prentice Hall. ISBN 978-0-13-086373-7.
  26. Powers, David MW (1991). Quicksort y Radixsort paralelizados con aceleración óptima . Actas de la Conferencia Internacional sobre Tecnologías de Computación Paralela. CiteSeerX 10.1.1.57.9071 . 
  27. El otro puede tener 1 elemento o estar vacío (tener 0 elementos), dependiendo de si el pivote está incluido en una de las subparticiones, como en la rutina de partición de Hoare, o está excluido de ambas, como en la rutina de Lomuto.
  28. Motwani, Rajeev; Raghavan, Prabhakar (25 de agosto de 1995). Algoritmos aleatorios . Cambridge University Press. ISBN 9780521474658.
  29. Ďurian, Branislav. "Quicksort sin pila". Fundamentos matemáticos de la informática 1986: Actas del 12.º simposio . MFCS 1986. Bratislava, Checoslovaquia: Springer Berlin Heidelberg.
  30. Edelkamp, ​​Stefan; Weiß, Armin (7–8 de enero de 2019). Ordenación eficiente en el peor de los casos con QuickMergesort . ALENEX 2019: 21.º Taller sobre Ingeniería de Algoritmos y Experimentos. San Diego. arXiv : 1811.99833 . doi : 10.1137/1.9781611975499.1 . ISBN 978-1-61197-549-9En instancias pequeñas , Heapsort ya es considerablemente más lento que Quicksort (en nuestros experimentos, más del 30 % para n = 2 10 ) y en instancias más grandes sufre de su mal comportamiento de caché (en nuestros experimentos, más de ocho veces más lento que Quicksort para ordenar 2 28 elementos).
  31. Hsieh, Paul (2004). "La clasificación revisitada" . azillionmonkeys.com.
  32. MacKay, David (diciembre de 2005). "Heapsort, Quicksort y entropía" . Archivado del original el 1 de abril de 2009.
  33. 1 2 Kutenin, Danila (20 de abril de 2022). "Cambiando std::sort a la escala de Google y más allá" . Experimental chill .
  34. Wild, Sebastian; Nebel, Markus E. (2012). Análisis del caso promedio del quicksort de doble pivote de Java 7. Simposio Europeo sobre Algoritmos. arXiv : 1310.7409 . Bibcode : 2013arXiv1310.7409W .
  35. Yaroslavskiy, Vladimir (2009). "Algoritmo Quicksort de doble pivote" (PDF) . Archivado del original (PDF) el 2 de octubre de 2015.
  36. Wild, S.; Nebel, M.; Reitzig, R.; Laube, U. (7 de enero de 2013). Ingeniería del algoritmo Quicksort de doble pivote de Java 7 utilizando MaLiJAn . Actas. Sociedad de Matemáticas Industriales y Aplicadas. págs. 55–69 . doi : 10.1137/1.9781611972931.5 . ISBN  978-1-61197-253-5.
  37. "Arreglos" . Plataforma Java SE 7. Oracle . Consultado el 4 de septiembre de 2014 .
  38. Wild, Sebastian (3 de noviembre de 2015). "¿Por qué es rápido el algoritmo Quicksort de doble pivote?". arXiv : 1511.01138 [ cs.DS ].
  39. Kushagra, Shrinu; López-Ortiz, Alejandro; Qiao, Aurick; Munro, J. Ian (2014). Quicksort multipivote: teoría y experimentos . Actas del Taller sobre Ingeniería y Experimentos de Algoritmos (ALENEX). doi : 10.1137/1.9781611973198.6 .
  40. ^ Kushagra, Shrinu; López-Ortiz, Alejandro; Munro, J. Ian; Qiao, Aurick (7 de febrero de 2014). Clasificación rápida multipivote: teoría y experimentos (PDF) (Presentación del seminario). Waterloo, Ontario .
  41. Motzkin, D.; Hansen, CL (1982), "Una clasificación externa eficiente con requisitos mínimos de espacio", International Journal of Computer and Information Sciences , 11 (6): 381– 396, doi : 10.1007/BF00996816 , S2CID 6829805 
  42. David MW Powers, Unificación paralela: complejidad práctica , Taller de arquitectura informática de Australasia, Universidad de Flinders, enero de 1995
  43. Kaligosi, Kanela; Sanders, Peter (11–13 de septiembre de 2006). Cómo las predicciones erróneas de ramificación afectan a Quicksort (PDF) . ESA 2006: 14.º Simposio Europeo Anual sobre Algoritmos. Zúrich . doi : 10.1007/11841036_69 .
  44. Edelkamp, ​​Stefan; Weiß, Armin (22 de abril de 2016). "BlockQuicksort: cómo las predicciones erróneas de las ramas no afectan a Quicksort". arXiv : 1604.06697 [ cs.DS ].
  45. Richard Cole, David C. Kandathil: "Análisis del caso promedio de los algoritmos de ordenación por partición" , Simposio Europeo sobre Algoritmos, 14-17 de septiembre de 2004, Bergen, Noruega. Publicado en: Lecture Notes in Computer Science 3221, Springer Verlag, pp. 240-251.

Referencias

  • Sedgewick, R. (1978). "Implementación de programas Quicksort". Comm. ACM . 21 (10): 847– 857. doi : 10.1145/359619.359631 . S2CID 10020756 . 
  • Dean, BC (2006). "Un análisis simple del tiempo de ejecución esperado para algoritmos aleatorios de 'divide y vencerás'" . Matemáticas Aplicadas Discretas . 154 : 1–5 . doi : 10.1016/j.dam.2005.07.005 .
  • Hoare, CAR (1961). "Algoritmo 63: Partición". Comm. ACM . 4 (7): 321. doi : 10.1145/366622.366642 . S2CID 52800011 . 
  • Hoare, CAR (1961). "Algoritmo 65: Find". Comm. ACM . 4 (7): 321– 322. doi : 10.1145/366622.366647 .
  • Hoare, COCHE (1962). "Clasificación rápida" . Computadora. J. 5 (1): 10– 16. doi : 10.1093/comjnl/5.1.10 .(Reimpreso en Hoare y Jones: Ensayos sobre ciencias de la computación , 1989).
  • Musser, David R. (1997). "Algoritmos de clasificación y selección introspectivos" . Software: Practice and Experience . 27 (8): 983– 993. doi : 10.1002/(SICI)1097-024X(199708)27:8 < 983::AID-SPE117 > 3.0.CO ; 2-# .
  • Donald Knuth . El arte de la programación informática , Volumen 3: Ordenación y búsqueda , Tercera edición. Addison-Wesley, 1997. ISBN 0-201-89685-0. Páginas 113–122 de la sección 5.2.2: Ordenación por intercambio.
  • Thomas H. Cormen , Charles E. Leiserson , Ronald L. Rivest y Clifford Stein . Introducción a los algoritmos , segunda edición. MIT Press y McGraw-Hill , 2001. ISBN 0-262-03293-7Capítulo 7: Quicksort, págs.  145–164.
  • Moller, Faron . "Análisis de Quicksort" (PDF) . Archivado del original (PDF) el 7 de julio de 2022. Recuperado el 3 de diciembre de 2024 .(CS 332: Diseño de algoritmos. Departamento de Informática, Universidad de Swansea .)
  • Martínez, C.; Roura, S. (2001). "Estrategias de muestreo óptimas en Quicksort y Quickselect". SIAM J. Comput. 31 (3): 683– 705. CiteSeerX 10.1.1.17.4954 . doi : 10.1137/S0097539700382108 . 
  • Bentley, JL; McIlroy, MD (1993). "Ingeniería de una función de ordenación". Software: Práctica y experiencia . 23 (11): 1249– 1265. CiteSeerX 10.1.1.14.8162 . doi : 10.1002/spe.4380231105 . S2CID 8822797 .  
  • "Algoritmos de ordenación animados: Ordenación rápida" . Archivado del original el 2 de marzo de 2015. Consultado el 25 de noviembre de 2008 .– demostración gráfica
  • "Algoritmos de ordenación animados: Ordenación rápida (partición de 3 vías)" . Archivado del original el 6 de marzo de 2015. Consultado el 25 de noviembre de 2008 .
  • Estructuras de datos abiertas – Sección 11.1.2 – Quicksort , Pat Morin
  • Ilustración interactiva de Quicksort , con explicación del código paso a paso.
  • Clasificación rápida con Quicksort , una guía completa para principiantes.
Retrieved from "https://en.wikipedia.org/w/index.php?title=Quicksort&oldid=1352961874"