Quicksort es un algoritmo de clasificaciónin situ. Desarrollado por el científico informático británico Tony Hoare en 1959 y publicado en 1961, sigue siendo un algoritmo de uso común para la clasificación. Cuando se implementa bien, puede ser algo más rápido que la ordenación combinada y aproximadamente dos o tres veces más rápido que la ordenación enpila.
Quicksort es un algoritmo de divide y vencerás. Funciona seleccionando un elemento 'pivote' de la matriz y dividiendo los otros elementos en dos submatrices, según sean menores o mayores que el pivote. Por esta razón, a veces se denomina ordenación de intercambio de particiones. A continuación, las submatrices se ordenan de forma recursiva. Esto se puede hacer en el lugar, requiriendo pequeñas cantidades adicionales de memoria para realizar la clasificación.
Quicksort es un tipo de comparación, lo que significa que puede clasificar elementos de cualquier tipo para los que se define una relación "menor que" (formalmente, un orden total ). Las implementaciones eficientes de Quicksort no son una ordenación estable, lo que significa que no se conserva el orden relativo de elementos de ordenación iguales.
El algoritmo de clasificación rápida fue desarrollado en 1959 por Tony Hoare mientras era un estudiante visitante en la Universidad Estatal de Moscú. En ese momento, Hoare estaba trabajando 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 en ruso antes de buscarlas en un diccionario ruso-inglés, que estaba en orden alfabético en cinta magnética. Después de reconocer que su primera idea, el tipo de inserción, sería lenta, se le ocurrió una nueva idea. Escribió la parte de la partición en Mercury Autocode, pero tuvo problemas para manejar la lista de segmentos sin clasificar. A su regreso a Inglaterra, se le pidió que escribiera código para Shellsort. Hoare le mencionó a su jefe que conocía un algoritmo más rápido y su jefe apostó seis peniques a que no. Su jefe finalmente aceptó que había perdido la apuesta. Más tarde, Hoare se enteró de ALGOL y su capacidad para hacer recursividad, lo que le permitió publicar el código en Communications of the Association for Computing Machinery, la principal revista de ciencias de la computación de la época.
Quicksort ganó una adopción generalizada, apareciendo, por ejemplo, en Unix como la subrutina de ordenación de bibliotecas predeterminada. Por lo tanto, prestó su nombre a la subrutina qsort de la biblioteca estándar de C y en la implementación de referencia de Java.
La tesis de doctorado 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 pivotes, incluido Samplesort, partición adaptativa de Van Emden, así como la derivación del número esperado de comparaciones y intercambios. Jon Bentley y Doug McIlroy incorporaron varias mejoras para su uso en bibliotecas de programación, incluida una técnica para tratar con elementos iguales y un esquema de pivote conocido como pseudomedia de nueve, donde una muestra de nueve elementos se divide en grupos de tres y luego la mediana de la se eligen tres medianas de tres grupos. 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. Bentley describió Quicksort como el "código más hermoso que jamás haya 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 hace tres veces más intercambios en promedio y se degrada al tiempo de ejecución O ( n 2) cuando todos los elementos son iguales.
En 2009, Vladimir Yaroslavskiy propuso una nueva implementación de Quicksort utilizando dos pivotes en lugar de uno. En las listas de correo de la biblioteca principal de Java, inició una discusión afirmando que su nuevo algoritmo era superior al método de clasificación de la biblioteca en tiempo de ejecución, que en ese momento se basaba en la variante ampliamente utilizada y cuidadosamente ajustada del Quicksort clásico de Bentley y McIlroy. Quicksort de Yaroslavskiy ha sido elegido como el nuevo algoritmo de clasificación predeterminado en la biblioteca de tiempo de ejecución de Java 7 de Oracle después de extensas pruebas empíricas de rendimiento.
Algoritmo
Ejemplo completo de clasificación rápida en un conjunto aleatorio de números. El elemento sombreado es el pivote. Siempre se elige como último elemento de la partición. Sin embargo, elegir siempre el último elemento de la partición como pivote de esta manera da como resultado un rendimiento deficiente ( O ( n 2)) en matrices ya ordenadas o matrices de elementos idénticos. Dado que las submatrices de elementos ordenados / idénticos surgen mucho hacia el final de un procedimiento de clasificación en un conjunto grande, las versiones del algoritmo de ordenación rápida que eligen el pivote como elemento intermedio se ejecutan mucho más rápidamente que el algoritmo descrito en este diagrama en grandes conjuntos de números.
Quicksort es un tipo de algoritmo de divide y vencerás para ordenar una matriz, basado en una rutina de partición; los detalles de esta partición pueden variar un poco, por lo que quicksort es realmente una familia de algoritmos estrechamente relacionados. Aplicado a un rango de al menos dos elementos, la partición produce una división en dos subrangos consecutivos no vacíos, de tal manera que ningún elemento del primer subrango es mayor que cualquier elemento del segundo subrango. Después de aplicar esta partición, quicksort ordena de forma recursiva los sub-rangos, posiblemente después de excluir de ellos un elemento en el punto de división que en este punto se sabe que ya se encuentra en su ubicación final. Debido a su naturaleza recursiva, la ordenación rápida (como la rutina de partición) debe formularse de manera que se pueda llamar para un rango dentro de una matriz más grande, incluso si el objetivo final es ordenar una matriz completa. Los pasos para la ordenación rápida en el lugar son:
Si el rango tiene menos de dos elementos, regrese inmediatamente ya que no hay nada que hacer. Posiblemente, para otras longitudes muy cortas, se aplica un método de clasificación especial y se omiten el resto de estos pasos.
De lo contrario, elija un valor, llamado pivote, que ocurra en el rango (la forma precisa de elegir depende de la rutina de partición y puede implicar aleatoriedad).
Particione el rango: reordene sus elementos, mientras determina un punto de división, de modo que todos los elementos con valores menores que el pivote vengan antes de la división, mientras que todos los elementos con valores mayores que el pivote vengan después de ella; los elementos que son iguales al pivote pueden ir en cualquier dirección. Dado que al menos una instancia del pivote está presente, la mayoría de las rutinas de partición aseguran que el valor que termina en el punto de división sea igual al pivote, y ahora esté en su posición final (pero la terminación de quicksort no depende de esto, siempre que se produzcan subrangos estrictamente más pequeños que el original).
Aplique de forma recursiva la clasificación rápida al subrango hasta el punto de división y al subrango después de él, posiblemente excluyendo de ambos rangos el elemento igual al pivote en el punto de división. (Si la partición produce un subrango posiblemente mayor cerca del límite donde se sabe que todos los elementos son iguales al pivote, estos también se pueden excluir).
La elección de la rutina de partición (incluida la selección de pivote) y otros detalles no especificados por completo anteriormente pueden afectar el rendimiento del algoritmo, posiblemente en gran medida para matrices de entrada específicas. Por lo tanto, al discutir la eficiencia de la clasificación rápida, es necesario especificar estas opciones primero. Aquí mencionamos dos métodos de partición específicos.
Esquema de partición Lomuto
Este esquema es atribuido a Nico Lomuto y popularizado por Bentley en su libro Programming Pearls y Cormen et al. en su libro Introducción a los algoritmos. Este esquema elige un pivote que suele ser el último elemento de la matriz. El algoritmo mantiene el índice i mientras escanea la matriz usando otro índice j, de modo 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 con frecuencia en material introductorio, aunque es menos eficiente que el esquema original de Hoare, por ejemplo, cuando todos los elementos son iguales. Este esquema se degrada a O ( n 2) cuando la matriz ya está en orden. Se han propuesto varias variantes para mejorar el rendimiento, incluidas varias formas de seleccionar el pivote, tratar con elementos iguales, usar otros algoritmos de clasificación, como la clasificació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 una matriz A se puede expresar como:
// Sorts a (portion of an) array, divides it into partitions, then sorts those algorithm quicksort(A, lo, hi) is // If indices are in correct order if lo gt;= 0 amp;amp; hi gt;= 0 amp;amp; lo lt; hi then // Partition array and get pivot index p := partition(A, lo, hi) // Sort the two partitions quicksort(A, lo, p - 1) // Left side of pivot quicksort(A, p + 1, hi) // Right side of pivot // Divides array into two partitions algorithm partition(A, lo, hi) is pivot := A[hi] // The pivot must be the last element // Pivot index i := lo - 1 for j := lo to hi do // If the current element is less than or equal to the pivot if A[j] lt;= pivot then // Move the pivot index forward i := i + 1 // Swap the current element with the element at the pivot swap A[i] with A[j] return i // the pivot index
La clasificación de toda la matriz se realiza mediante clasificación rápida (A, 0, longitud (A) - 1).
Esquema de partición Hoare
Una demostración animada de Quicksort usando 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 que se está comparando con ( pivot).
El esquema de partición original descrito por Tony Hoare usa dos punteros (índices en el rango) que comienzan en ambos extremos de la matriz que se está particionando, luego se mueven uno hacia el otro, hasta que detectan una inversión: un par de elementos, uno mayor que el límite. (Términos de Hoare para el valor de pivote) en el primer puntero, y uno menos que el límite en el segundo puntero; si en este punto el primer puntero todavía está antes del segundo, estos elementos están en el orden incorrecto entre sí y luego se intercambian. 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 (los primeros puntos 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 se puede excluir de los dos subrangos formados). Con esta formulación es posible que un subrango resulte ser todo el rango original, lo que evitaría que el algoritmo avance. Por lo tanto, Hoare estipula que al final, el sub-rango que contiene el elemento pivote (que todavía está en su posición original) puede reducirse en tamaño excluyendo ese pivote, después (si es necesario) intercambiarlo con el elemento del sub-rango más cercano a la separación; por lo tanto, se asegura la terminación de la clasificación rápida.
Con respecto a esta descripción original, las implementaciones a menudo presentan variaciones menores pero importantes. En particular, el esquema que se presenta a continuación incluye elementos iguales al pivote entre los candidatos para una inversión (por lo que las pruebas "mayor o igual que" y "menor o igual" se utilizan en lugar de "mayor que" respectivamente "menor que"; ya que la formulación utiliza do... while en lugar de repetir... hasta que en realidad se refleja en el uso de operadores de comparación estrictos). Si bien no hay ninguna razón para intercambiar elementos iguales al límite, este cambio permite omitir las pruebas en los punteros, que de lo contrario son necesarios para garantizar que no se salgan del rango. De hecho, dado que al menos una instancia del valor de pivote está presente en el rango, el primer avance de cualquiera de los punteros no puede pasar a través de esta instancia si se usa una prueba inclusiva; una vez que se realiza un intercambio, estos elementos intercambiados ahora están estrictamente por delante del puntero que los encontró, evitando que ese puntero se escape. (Esto último es cierto independientemente de la prueba utilizada, por lo que sería posible utilizar la prueba inclusiva solo cuando se busque la primera inversión. Sin embargo, el uso de una prueba inclusiva en todas partes también garantiza que se encuentre una división cerca de la mitad cuando todos los elementos en los rangos son iguales, lo que proporciona una ganancia de eficiencia importante para clasificar matrices con muchos elementos iguales). El riesgo de producir una separación sin avance se evita de una manera diferente a la descrita por Hoare. Tal separación solo puede producirse cuando no se encuentran inversiones, con ambos punteros avanzando hacia el elemento pivote en la primera iteración (luego se considera que se han cruzado y no se produce ningún intercambio). La división devuelta es posterior a la posición final del segundo puntero, por lo que el caso a evitar es donde el pivote es el elemento final del rango y todos los demás son más pequeños que él. Por lo tanto, la elección del pivote debe evitar el elemento final (en la descripción de Hoare podría ser cualquier elemento del rango); esto se hace aquí redondeando hacia abajo la posición media, usando la floorfunción. Esto ilustra que el argumento a favor de la corrección de una implementación del esquema de partición de Hoare puede ser sutil y es fácil equivocarse.
// Sorts a (portion of an) array, divides it into partitions, then sorts those algorithm quicksort(A, lo, hi) is if lo gt;= 0 amp;amp; hi gt;= 0 amp;amp; lo lt; hi then p := partition(A, lo, hi) quicksort(A, lo, p) // Note: the pivot is now included quicksort(A, p + 1, hi) // Divides array into two partitions algorithm partition(A, lo, hi) is // Pivot value pivot := A[ floor((hi + lo) / 2) ] // The value in the middle of the array // Left index i := lo - 1 // Right index j := hi + 1 loop forever // Move the left index to the right at least once and while the element at // the left index is less than the pivot do i := i + 1 while A[i] lt; pivot // Move the right index to the left at least once and while the element at // the right index is greater than the pivot do j := j - 1 while A[j] gt; pivot // If the indices crossed, return if i ≥ j then return j // Swap the elements at the left and right indices swap A[i] with A[j]
Toda la matriz se ordena por orden rápido (A, 0, longitud (A) - 1).
El esquema de Hoare es más eficiente que el esquema de partición de Lomuto porque hace tres veces menos intercambios en promedio. Además, como se mencionó, la implementación dada crea una partición balanceada incluso cuando todos los valores son iguales, lo que no ocurre con el esquema de Lomuto. 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 primer o último elemento. Sin embargo, con el elemento intermedio como pivote, los datos ordenados resultan con (casi) ningún intercambio en particiones de igual tamaño, lo que conduce al mejor comportamiento de caso de Quicksort, es decir, O ( n log ( n)). Como otros, la partición de Hoare no produce un tipo estable. En este esquema, la ubicación final del pivote no está necesariamente en el índice que se devuelve, ya que el pivote y los elementos iguales al pivote pueden terminar en cualquier lugar dentro de la partición después de un paso de partición, y no pueden ordenarse hasta el caso base de un la partición con un solo elemento se alcanza mediante recursividad. Los siguientes dos segmentos en los que se repite el algoritmo principal son (lo..p) (elementos ≤ pivote) y (p + 1..hi) (elementos ≥ pivote) en contraposición a (lo..p-1) y (p + 1..hi) como en el esquema de Lomuto.
Problemas de implementación
Elección de pivote
En las primeras versiones de quicksort, el elemento más a la izquierda de la partición a menudo se elegía como elemento pivote. Desafortunadamente, esto causa el peor de los casos en arreglos ya ordenados, que es un caso de uso bastante común. El problema se resolvió fácilmente eligiendo un índice aleatorio para el pivote, eligiendo el índice medio de la partición o (especialmente para particiones más largas) eligiendo la mediana del primer, medio y último elemento de la partición para el pivote (como recomienda Sedgewick ). Esta regla de "mediana de tres" contrarresta el caso de la entrada ordenada (o ordenada al revés) y proporciona una mejor estimación del pivote óptimo (la verdadera mediana) que la selección de un solo elemento, cuando no hay información sobre el orden de la la entrada es conocida.
Fragmento de código de mediana de tres para la partición Lomuto:
mid := ⌊(lo + hi) / 2⌋ if A[mid] lt; A[lo] swap A[lo] with A[mid] if A[hi] lt; A[lo] swap A[lo] with A[hi] if A[mid] lt; A[hi] swap A[mid] with A[hi] pivot := A[hi]
Primero coloca una mediana A[hi], luego ese nuevo valor de A[hi]se usa para un pivote, como en un algoritmo básico presentado anteriormente.
Específicamente, el número esperado de comparaciones necesarias para ordenar n elementos (ver § Análisis de ordenación rápida aleatoria) con selección de pivote aleatorio es 1.386 n log n. La pivotación mediana de tres reduce esto a C n, 2 ≈ 1.188 n log n, a expensas de un aumento del tres por ciento en el número esperado de intercambios. Una regla de pivote aún más fuerte, para matrices más grandes, es elegir el ninther, una mediana recursiva de tres (Mo3), definida como
La selección de un elemento pivote también se complica por la existencia de un desbordamiento de enteros. Si los índices de límite del subarreglo que se está ordenando son lo suficientemente grandes, la expresión ingenua para el índice medio, ( lo + hi) / 2, provocará un desbordamiento y proporcionará un índice de pivote no válido. Esto puede superarse utilizando, por ejemplo, lo + ( hi - lo) / 2 para indexar el elemento intermedio, a costa de una aritmética más compleja. Surgen problemas similares en algunos otros métodos de selección del elemento pivote.
Elementos repetidos
Con un algoritmo de partición como el esquema de partición de Lomuto descrito anteriormente (incluso uno que elige buenos valores de pivote), quicksort exhibe un rendimiento deficiente para las 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 elimina). En consecuencia, el esquema de partición de Lomuto toma un tiempo cuadrático para ordenar una matriz de valores iguales. Sin embargo, con un algoritmo de partición como el esquema de partición de Hoare, los elementos repetidos generalmente dan como resultado una mejor partición, 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 memoria caché reduciendo los gastos generales de permuta). En el caso de que todos los elementos sean iguales, el esquema de partición de Hoare intercambia elementos innecesariamente, pero la partición en sí es el mejor de los casos, como se indica 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 ), se puede usar una rutina alternativa de partición en 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 llaman a esto una "partición gruesa" y ya se implementó en el qsort de la versión 7 de Unix ). Los valores iguales al pivote ya están ordenados, por lo que solo las particiones menor que y mayor que deben ser recursivas ordenados. En pseudocódigo, el algoritmo de ordenación rápida se convierte en
algorithm quicksort(A, lo, hi) is if lo lt; hi then p := pivot(A, lo, hi) left, right := partition(A, p, lo, hi) // note: multiple return values quicksort(A, lo, left - 1) quicksort(A, right + 1, hi)
El partitionalgoritmo devuelve índices al primer elemento ('más a la izquierda') y al último ('más a la derecha') de la partición del medio. Todos los elementos de la partición son iguales py, 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 caso para el algoritmo ocurre ahora cuando todos los elementos son iguales (o se eligen de un pequeño conjunto de k ≪ n elementos). En el caso de todos los elementos iguales, la ordenación rápida modificada realizará solo dos llamadas recursivas en subarreglos vacíos y, por lo tanto, finalizará en tiempo lineal (asumiendo que la partitionsubrutina no toma más tiempo que el tiempo lineal).
Optimizaciones
Otras dos optimizaciones importantes, también sugeridas por Sedgewick y ampliamente utilizadas en la práctica, son:
Para asegurarse de que se usa como máximo el espacio O (log n), recurra primero al lado más pequeño de la partición, luego use una llamada de cola para recurrir al otro, o actualice los parámetros para que ya no incluyan el lado más pequeño ahora ordenado, y iterar para ordenar el lado más grande.
Cuando el número de elementos está por debajo de algún umbral (quizás diez elementos), cambie a un algoritmo de clasificación no recursivo, como el ordenamiento 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 más antigua de la optimización anterior: cuando el número de elementos es menor que el umbral k, simplemente deténgase; luego, una vez que se haya procesado toda la matriz, realice la ordenación por inserción en ella. Detener la recursividad antes de tiempo deja la matriz k- ordenada, lo que significa que cada elemento está en la mayoría de las k posiciones de su posición final ordenada. En este caso, la ordenación por inserción tarda O ( kn) en finalizar la ordenación, que es lineal si k es una constante. En comparación con la optimización de "muchos tipos pequeños", esta versión puede ejecutar menos instrucciones, pero hace un uso subóptimo de las memorias caché en las computadoras modernas.
Paralelización
La fórmula de dividir y conquistar de Quicksort lo hace apto para la paralelización mediante el paralelismo de tareas. El paso de partición se logra mediante el uso de un algoritmo de suma de prefijo paralelo para calcular un índice para cada elemento de la matriz en su sección de la matriz particionada. Dada 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 de borrador adicional. Una vez que se ha particionado la matriz, las dos particiones se pueden ordenar de forma recursiva en paralelo. Suponiendo una elección ideal de pivotes, la ordenación rápida en 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.
La ordenación rápida tiene algunas desventajas en comparación con los algoritmos de ordenación alternativos, como la ordenación por fusión, que complican su eficiente paralelización. La profundidad del árbol de divide y vencerás de quicksort impacta directamente en la escalabilidad del algoritmo, y esta profundidad depende en gran medida de la elección de pivote del algoritmo. Además, es difícil paralelizar el paso de partición de manera eficiente en el lugar. El uso de espacio temporal simplifica el paso de partición, pero aumenta la huella de memoria del algoritmo y los gastos generales constantes.
Otros algoritmos de clasificación en paralelo más sofisticados pueden lograr límites de tiempo aún mejores. Por ejemplo, en 1991 David Powers describió una ordenación rápida en paralelo (y una ordenación de radix relacionada) que puede operar en tiempo O (log n) en una PRAM (máquina de acceso aleatorio en paralelo) CRCW (lectura y escritura concurrentes) con n procesadores por realizar particiones implícitamente.
Análisis formal
Análisis del peor de los casos
La partición más desequilibrada ocurre cuando una de las sublistas devueltas por la rutina de partición es de tamaño n - 1. Esto puede ocurrir si el pivote es 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 como se describe arriba) cuando todos los elementos son iguales.
Si esto sucede repetidamente en cada partición, entonces cada llamada recursiva procesa una lista de tamaño uno menos que la lista anterior. En consecuencia, podemos hacer n - 1 llamadas anidadas antes de alcanzar 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 hace que O ( n - i) funcione para hacer la partición y, en ese caso, la ordenación rápida toma O ( n 2) tiempo.
Análisis del mejor de los casos
En el caso más equilibrado, cada vez que realizamos una partición, dividimos la lista en dos partes casi iguales. Esto significa que cada llamada recursiva procesa una lista de la mitad del tamaño. En consecuencia, solo podemos hacer log 2 n llamadas anidadas antes de alcanzar una lista de tamaño 1. Esto significa que la profundidad del árbol de llamadas es log 2 n. Pero no hay dos llamadas al 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 usa solo O ( n log n) tiempo.
Análisis de casos promedio
Para ordenar una matriz de n elementos distintos, la ordenación rápida toma O ( n log n) tiempo de expectativa, promediado sobre todos los n ! permutaciones de n elementos con igual probabilidad. Aquí enumeramos tres pruebas comunes de esta afirmación que brindan diferentes conocimientos sobre el funcionamiento de quicksort.
Usando percentiles
Si cada pivote tiene un rango en algún lugar en el medio del 50 por ciento, es decir, entre el percentil 25 y el percentil 75, entonces divide los elementos con al menos el 25% y como máximo el 75% en cada lado. Si pudiéramos elegir consistentemente tales pivotes, solo tendríamos que dividir la lista la mayoría de las veces antes de llegar a listas de tamaño 1, lo que produciría 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 medio del 50 por ciento. Sin embargo, cuando partimos de una permutación aleatoria, en cada llamada recursiva el pivote tiene un rango aleatorio en su lista, por lo que está en el medio 50 por ciento aproximadamente la mitad del tiempo. Eso es suficientemente bueno. Imagine que se lanza una moneda: cara significa que el rango del pivote está en el medio del 50 por ciento, cola significa que no lo es. Ahora imagina que la moneda se voltea una y otra vez hasta que sale k caras. Aunque esto puede llevar mucho tiempo, en promedio solo se requieren 2 k lanzamientos, y la posibilidad de que la moneda no obtenga k caras después de 100 k lanzamientos es muy improbable (esto se puede hacer más riguroso usando los límites de Chernoff ). Con el mismo argumento, la recursividad de Quicksort terminará en promedio a una profundidad de llamada de solo. 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 del medio; si lo golpeamos una fracción constante de las veces, eso es suficiente para la complejidad deseada.
Usar 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 de ordenación rápida implica trabajo 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
En el caso más equilibrado, una sola llamada de ordenación rápida implica O ( n) trabajo más dos llamadas recursivas en listas de tamaño n / 2, por lo que la relación de recurrencia es
A continuación se muestra el esquema de una prueba formal de la complejidad de tiempo esperado O ( n log n). Suponga que no hay duplicados, ya que los duplicados podrían manejarse con pre y posprocesamiento de tiempo lineal, o considerar los casos más fáciles que los analizados. Cuando la entrada es una permutación aleatoria, el rango del pivote es aleatorio uniforme de 0 a n - 1. Entonces, las partes resultantes de la partición tienen tamaños i y n - i - 1, e i es aleatorio uniforme de 0 a n - 1. Entonces, promediando 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: