El sitio web de Haskell presenta una función de ordenación rápida de 5 líneas muy atractiva , como se ve a continuación.
quicksort [] = []
quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
where
lesser = filter (< p) xs
greater = filter (>= p) xs
También incluyen una "clasificación rápida verdadera en C" .
// To sort array a[] of size n: qsort(a,0,n-1)
void qsort(int a[], int lo, int hi)
{
int h, l, p, t;
if (lo < hi) {
l = lo;
h = hi;
p = a[hi];
do {
while ((l < h) && (a[l] <= p))
l = l+1;
while ((h > l) && (a[h] >= p))
h = h-1;
if (l < h) {
t = a[l];
a[l] = a[h];
a[h] = t;
}
} while (l < h);
a[hi] = a[l];
a[l] = p;
qsort( a, lo, l-1 );
qsort( a, l+1, hi );
}
}
Un enlace debajo de la versión C dirige a una página que dice 'El ordenamiento rápido citado en Introducción no es el ordenamiento rápido "real" y no escala para listas más largas como lo hace el código c'.
¿Por qué la función Haskell anterior no es una verdadera clasificación rápida? ¿Cómo no se puede escalar para listas más largas?
O(N^2)
tiempo de ejecución.Respuestas:
La verdadera clasificación rápida tiene dos aspectos hermosos:
El ejemplo corto de Haskell demuestra (1), pero no (2). ¡Cómo se hace (2) puede no ser obvio si aún no conoce la técnica!
fuente
Verdadero ordenamiento rápido en Haskell:
fuente
unstablePartition
es muy similar apartition
forquicksort
, pero no garantiza que el elemento enm
la posición sea justop
.Aquí hay una transliteración del código C de clasificación rápida "verdadero" a Haskell. Prepárate.
Eso fue divertido, ¿no? De hecho, corté este tamaño
let
al principio, así comowhere
al final de la función, definiendo todos los ayudantes para hacer que el código anterior sea algo bonito.Y aquí, una prueba tonta para ver si funciona.
No escribo código imperativo muy a menudo en Haskell, así que estoy seguro de que hay muchas formas de limpiar este código.
¿Y qué?
Notará que el código anterior es muy, muy largo. La esencia es tan larga como el código C, aunque cada línea suele ser un poco más detallada. Esto se debe a que C secretamente hace muchas cosas desagradables que podrías dar por sentado. Por ejemplo
a[l] = a[h];
,. Esto accede a las variables mutablesl
yh
, y luego accede a la matriz mutablea
y luego muta la matriz mutablea
. ¡Santa mutación, Batman! En Haskell, la mutación y el acceso a variables mutables son explícitos. El qsort "falso" es atractivo por varias razones, pero la principal de ellas es que no usa mutación; esta restricción autoimpuesta hace que sea mucho más fácil de entender de un vistazo.fuente
En mi opinión, decir que "no es una verdadera selección rápida" exagera el caso. Creo que es una implementación válida del algoritmo Quicksort , pero no particularmente eficiente.
fuente
Creo que el caso que intenta demostrar este argumento es que la razón por la que se usa comúnmente la ordenación rápida es que está en el lugar y, como resultado, es bastante compatible con el caché. Dado que no tiene esos beneficios con las listas de Haskell, su principal razón de ser se ha ido, y también podría usar la ordenación por fusión, que garantiza O (n log n) , mientras que con la ordenación rápida debe usar la aleatorización o complicada esquemas de particionamiento para evitar el tiempo de ejecución O (n 2 ) en el peor de los casos.
fuente
Gracias a la evaluación perezosa, un programa Haskell no hace (casi no puede ) hacer lo que parece.
Considere este programa:
En un lenguaje ávido, primero
quicksort
correría, luegoshow
, luegoputStrLn
. Los argumentos de una función se calculan antes de que esa función comience a ejecutarse.En Haskell, es todo lo contrario. La función comienza a ejecutarse primero. Los argumentos solo se calculan cuando la función realmente los usa. Y un argumento compuesto, como una lista, se calcula una pieza a la vez, a medida que se utiliza cada pieza.
Entonces, lo primero que sucede en este programa es que
putStrLn
comienza a ejecutarse.La implementación de GHC de
putStrLn
funciona copiando los caracteres del argumento String en un búfer de salida. Pero cuando entra en este bucle,show
aún no se ha ejecutado. Por lo tanto, cuando va a copiar el primer carácter de la cadena, Haskell evalúa la fracción deshow
y lasquicksort
llamadas necesarias para calcular ese carácter . LuegoputStrLn
pasa al siguiente personaje. Por lo que la ejecución de los tres funciones-putStrLn
,show
yquicksort
- se intercalan.quicksort
se ejecuta de forma incremental, dejando un gráfico de procesadores no evaluados a medida que avanza para recordar dónde se quedó.Ahora bien, esto es tremendamente diferente de lo que podría esperar si está familiarizado con, ya sabe, cualquier otro lenguaje de programación. No es fácil visualizar cómo se
quicksort
comporta realmente Haskell en términos de accesos a la memoria o incluso el orden de las comparaciones. Si solo pudiera observar el comportamiento, y no el código fuente, no reconocería lo que está haciendo como una clasificación rápida .Por ejemplo, la versión C de quicksort particiona todos los datos antes de la primera llamada recursiva. En la versión Haskell, el primer elemento del resultado se calculará (e incluso podría aparecer en su pantalla) antes de que termine de ejecutarse la primera partición, de hecho, antes de que se realice ningún trabajo
greater
.PD: El código de Haskell sería más parecido a una ordenación rápida si hiciera el mismo número de comparaciones que la ordenación rápida; el código tal como está escrito hace el doble de comparaciones porque
lesser
ygreater
se especifican para ser calculados de forma independiente, haciendo dos escaneos lineales a través de la lista. Por supuesto, en principio es posible que el compilador sea lo suficientemente inteligente como para eliminar las comparaciones adicionales; o el código podría cambiarse para usarData.List.partition
.PPS El ejemplo clásico de que los algoritmos de Haskell no se comportan como se esperaba es el tamiz de Eratóstenes para calcular números primos.
fuente
primes = unfoldr (\(p:xs)-> Just (p, filter ((> 0).(`rem` p)) xs)) [2..]
, su problema más inmediato sería quizás más claro. Y eso es antes de que consideremos cambiar al verdadero algoritmo de tamiz.putStrLn
una aplicación thunked deshow
a una aplicación thunked dequicksort
a una lista literal --- ¡y eso es exactamente lo que hace! (antes de la optimización --- ¡pero compare el código C con el ensamblador optimizado en algún momento!). ¿Quizás te refieres a "gracias a la evaluación perezosa, un programa Haskell no hace lo que hace un código similar en otros lenguajes"?Creo que la razón por la que la mayoría de la gente dice que la bonita ordenación rápida de Haskell no es una ordenación rápida "verdadera" es el hecho de que no está en el lugar; claramente, no puede serlo cuando se usan tipos de datos inmutables. Pero también existe la objeción de que no es "rápido": en parte debido al costoso ++, y también porque hay una fuga de espacio: te aferras a la lista de entrada mientras haces la llamada recursiva en los elementos menores, y en algunos casos, por ejemplo, cuando la lista disminuye, esto da como resultado un uso de espacio cuadrático. (Se podría decir que hacer que se ejecute en un espacio lineal es lo más cercano a "in situ" usando datos inmutables). Hay soluciones claras para ambos problemas, usando parámetros acumulativos, tuples y fusión; ver S7.6.1 de Richard Bird '
fuente
No es la idea de mutar elementos in situ en entornos puramente funcionales. Los métodos alternativos en este hilo con matrices mutables perdieron el espíritu de pureza.
Hay al menos dos pasos para optimizar la versión básica (que es la versión más expresiva) de clasificación rápida.
Optimiza la concatenación (++), que es una operación lineal, por acumuladores:
Optimice la clasificación ternaria rápida (partición de 3 vías, mencionada por Bentley y Sedgewick), para manejar elementos duplicados:
Combine 2 y 3, consulte el libro de Richard Bird:
O alternativamente si los elementos duplicados no son la mayoría:
Desafortunadamente, la mediana de tres no se puede implementar con el mismo efecto, por ejemplo:
porque todavía funciona mal en los siguientes 4 casos:
[1, 2, 3, 4, ...., n]
[n, n-1, n-2, ..., 1]
[m-1, m-2, ... 3, 2, 1, m + 1, m + 2, ..., n]
[n, 1, n-1, 2, ...]
Todos estos 4 casos se manejan bien con un enfoque imperativo de mediana de tres.
En realidad, el algoritmo de ordenación más adecuado para una configuración puramente funcional sigue siendo la ordenación por combinación, pero no la ordenación rápida.
Para obtener más detalles, visite mi escritura en curso en: https://sites.google.com/site/algoxy/dcsort
fuente
No existe una definición clara de lo que es y lo que no es una verdadera selección rápida.
Lo llaman no una verdadera ordenación rápida, porque no ordena en el lugar:
fuente
Porque tomar el primer elemento de la lista da como resultado un tiempo de ejecución muy malo. Utilice una mediana de 3: primero, medio, último.
fuente
O(n^2)
Pídale a cualquiera que escriba quicksort en Haskell y obtendrá básicamente el mismo programa: obviamente es quicksort. A continuación se muestran algunas ventajas y desventajas:
Ventaja: Mejora la ordenación rápida "verdadera" al ser estable, es decir, conserva el orden de secuencia entre elementos iguales.
Ventaja: Es trivial generalizar a una división de tres vías (<=>), que evita el comportamiento cuadrático debido a que algún valor ocurre O (n) veces.
Ventaja: es más fácil de leer, incluso si se tuviera que incluir la definición de filtro.
Desventaja: usa más memoria.
Desventaja: es costoso generalizar la elección del pivote mediante un muestreo adicional, lo que podría evitar el comportamiento cuadrático en ciertos ordenamientos de baja entropía.
fuente