Me gustaría agregar a las excelentes respuestas existentes algunas matemáticas sobre cómo se desempeña QuickSort cuando se desvía del mejor de los casos y la probabilidad de que eso sea, lo que espero ayude a las personas a comprender un poco mejor por qué el caso O (n ^ 2) no es real preocupación en las implementaciones más sofisticadas de QuickSort.
Fuera de los problemas de acceso aleatorio, hay dos factores principales que pueden afectar el rendimiento de QuickSort y ambos están relacionados con la forma en que el pivote se compara con los datos que se ordenan.
1) Un pequeño número de claves en los datos. Un conjunto de datos con el mismo valor se ordenará en n ^ 2 veces en un QuickSort de 2 particiones vainilla porque todos los valores, excepto la ubicación de pivote, se colocan en un lado cada vez. Las implementaciones modernas abordan esto mediante métodos como el uso de una clasificación de 3 particiones. Estos métodos se ejecutan en un conjunto de datos con el mismo valor en O (n) tiempo. Por lo tanto, el uso de dicha implementación significa que una entrada con un pequeño número de claves en realidad mejora el tiempo de rendimiento y ya no es una preocupación.
2) La selección de pivote extremadamente mala puede causar el peor de los casos. En un caso ideal, el pivote siempre será tal que el 50% de los datos es más pequeño y el 50% de los datos es más grande, de modo que la entrada se dividirá por la mitad durante cada iteración. Esto nos da n comparaciones y swaps veces log-2 (n) recursiones para el tiempo O (n * logn).
¿Cuánto afecta la selección de pivote no ideal al tiempo de ejecución?
Consideremos un caso en el que el pivote se elige consistentemente de tal manera que el 75% de los datos están en un lado del pivote. Todavía es O (n * logn) pero ahora la base del registro ha cambiado a 1 / 0.75 o 1.33. La relación en el rendimiento al cambiar de base siempre es una constante representada por log (2) / log (newBase). En este caso, esa constante es 2.4. Por lo tanto, esta calidad de elección de pivote tarda 2,4 veces más que la ideal.
¿Qué tan rápido empeora esto?
No muy rápido hasta que la opción de pivote se vuelva (consistentemente) muy mala:
- 50% en un lado: (caso ideal)
- 75% en un lado: 2.4 veces más largo
- 90% en un lado: 6.6 veces más largo
- 95% en un lado: 13.5 veces más largo
- 99% en un lado: 69 veces más
A medida que nos acercamos al 100% de un lado, la porción de registro de la ejecución se acerca a n y toda la ejecución se acerca asintóticamente a O (n ^ 2).
En una implementación ingenua de QuickSort, los casos como una matriz ordenada (para el pivote del primer elemento) o una matriz ordenada inversamente (para el último pivote del elemento) producirán de manera confiable un tiempo de ejecución O (n ^ 2) en el peor de los casos. Además, las implementaciones con una selección de pivote predecible pueden estar sujetas a ataques DoS por datos diseñados para producir la ejecución del peor de los casos. Las implementaciones modernas evitan esto mediante una variedad de métodos, como aleatorizar los datos antes de ordenarlos, elegir la mediana de 3 índices elegidos al azar, etc. Con esta aleatorización en la mezcla, tenemos 2 casos:
- Pequeño conjunto de datos. El peor de los casos es razonablemente posible, pero O (n ^ 2) no es catastrófico porque n es lo suficientemente pequeño como para que n ^ 2 también lo sea.
- Gran conjunto de datos. El peor de los casos es posible en teoría pero no en la práctica.
¿Qué tan probable es que veamos un desempeño terrible?
Las posibilidades son muy pequeñas. . Consideremos una especie de 5,000 valores:
Nuestra implementación hipotética elegirá un pivote utilizando una mediana de 3 índices elegidos al azar. Consideraremos que los pivotes que están en el rango del 25% -75% son "buenos" y los pivotes que están en el rango del 0% -25% o 75% -100% son "malos". Si observa la distribución de probabilidad utilizando la mediana de 3 índices aleatorios, cada recursión tiene una probabilidad de 11/16 de terminar con un buen pivote. Hagamos 2 supuestos conservadores (y falsos) para simplificar las matemáticas:
Los buenos pivotes siempre están exactamente en una división del 25% / 75% y funcionan en el caso ideal de 2.4 *. Nunca obtenemos una división ideal o una división mejor que 25/75.
Los malos pivotes son siempre el peor de los casos y esencialmente no contribuyen en nada a la solución.
Nuestra implementación de QuickSort se detendrá en n = 10 y cambiará a un tipo de inserción, por lo que requerimos 22 particiones pivote del 25% / 75% para dividir la entrada de valor de 5,000 hasta ese momento. (10 * 1.333333 ^ 22> 5000) O bien, requerimos 4990 pivotes en el peor de los casos. Tenga en cuenta que si acumulamos 22 pivotes buenos en cualquier momento , la clasificación se completará, por lo que el peor de los casos o cualquier cosa cercana requiere muy mala suerte. Si nos tomara 88 recursiones para lograr realmente los 22 buenos pivotes requeridos para ordenar a n = 10, eso sería 4 * 2.4 * caso ideal o aproximadamente 10 veces el tiempo de ejecución del caso ideal. ¿Cuán probable es que lo haría no logremos los 22 buenos pivotes requeridos después de 88 recursiones?
Las distribuciones de probabilidad binomiales pueden responder eso, y la respuesta es aproximadamente 10 ^ -18. (n es 88, k es 21, p es 0.6875) Su usuario tiene aproximadamente mil veces más probabilidades de ser alcanzado por un rayo en el 1 segundo que toma hacer clic en [CLASIFICAR] que ver que la clasificación de 5.000 elementos funciona peor de 10 * caso ideal. Esta posibilidad se reduce a medida que aumenta el conjunto de datos. Aquí hay algunos tamaños de matriz y sus posibilidades correspondientes de ejecutar más de 10 * ideal:
- Matriz de 640 elementos: 10 ^ -13 (requiere 15 buenos puntos de pivote de 60 intentos)
- Matriz de 5,000 artículos: 10 ^ -18 (requiere 22 pivotes buenos de 88 intentos)
- Matriz de 40,000 artículos: 10 ^ -23 (requiere 29 pivotes buenos de 116)
Recuerde que esto es con 2 supuestos conservadores que son peores que la realidad. Por lo tanto, el rendimiento real es aún mejor, y el equilibrio de la probabilidad restante está más cerca del ideal que no.
Finalmente, como otros han mencionado, incluso estos casos absurdamente inverosímiles se pueden eliminar cambiando a un montón si la pila de recursión es demasiado profunda. Entonces, el TLDR es que, para buenas implementaciones de QuickSort, el peor de los casos realmente no existe porque se ha diseñado y la ejecución se completa en tiempo O (n * logn).
qsort
, Pythonlist.sort
yArray.prototype.sort
JavaScript de Firefox son todos tipos de fusión mejorados. (GNU STLsort
usa Introsort en su lugar, pero eso podría deberse a que en C ++, el intercambio potencialmente gana mucho más que la copia)