O site de Haskell apresenta uma função quicksort de 5 linhas muito atraente , como mostrado abaixo.
quicksort [] = []
quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
where
lesser = filter (< p) xs
greater = filter (>= p) xs
Eles também incluem uma "classificação rápida verdadeira em 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 );
}
}
Um link abaixo da versão C direciona para uma página que afirma 'O quicksort citado na Introdução não é o quicksort' real 'e não escalona para listas mais longas como o código c faz.'
Por que a função Haskell acima não é uma classificação rápida verdadeira? Como ele não consegue escalar para listas mais longas?
O(N^2)
tempo de execução.Respostas:
O verdadeiro quicksort tem dois belos aspectos:
O exemplo curto de Haskell demonstra (1), mas não (2). Como (2) é feito pode não ser óbvio se você ainda não conhece a técnica!
fonte
Quicksort local verdadeiro em Haskell:
fonte
unstablePartition
é muito semelhante apartition
forquicksort
, mas não garante que o elemento nam
posição seja justop
.Aqui está uma transliteração do "verdadeiro" código quicksort C para Haskell. Prepare-se.
Foi divertido, não foi? Na verdade, cortei esse tamanho grande
let
no início, bem comowhere
no final da função, definindo todos os auxiliares para tornar o código anterior um tanto bonito.E aqui, um teste idiota para ver se funciona.
Não escrevo código imperativo com muita frequência em Haskell, então tenho certeza de que há muitas maneiras de limpar esse código.
E daí?
Você notará que o código acima é muito, muito longo. O coração disso é quase tão longo quanto o código C, embora cada linha seja um pouco mais prolixa. Isso ocorre porque C secretamente faz muitas coisas desagradáveis que você pode considerar certas. Por exemplo
a[l] = a[h];
,. Isso acessa as variáveis mutáveisl
eh
, em seguida, acessa a matriz mutávela
e altera a matriz mutávela
. Santa mutação, batman! Em Haskell, a mutação e o acesso a variáveis mutáveis são explícitos. O qsort "falso" é atraente por vários motivos, mas o principal deles é que não usa mutação; essa restrição autoimposta torna muito mais fácil de entender à primeira vista.fonte
Em minha opinião, dizer que "não é um verdadeiro achado" exagera o caso. Acho que é uma implementação válida do algoritmo Quicksort , mas não particularmente eficiente.
fonte
Acho que o caso que esse argumento tenta apresentar é que a razão pela qual o quicksort é comumente usado é que ele está no local e, como resultado, é bastante amigável ao cache. Como você não tem esses benefícios com as listas de Haskell, sua principal razão de ser se foi, e você também pode usar merge sort, que garante O (n log n) , enquanto com quicksort você precisa usar randomização ou esquemas de particionamento para evitar o tempo de execução O (n 2 ) no pior caso.
fonte
Graças à avaliação preguiçosa, um programa Haskell não (quase não pode ) fazer o que parece que faz.
Considere este programa:
Em uma linguagem ansiosa, primeiro
quicksort
iria correr, entãoshow
, entãoputStrLn
. Os argumentos de uma função são calculados antes que a função comece a ser executada.Em Haskell, é o oposto. A função começa a funcionar primeiro. Os argumentos são calculados apenas quando a função realmente os usa. E um argumento composto, como uma lista, é calculado uma parte de cada vez, conforme cada parte dele é usada.
Então, a primeira coisa que acontece neste programa é que
putStrLn
começa a funcionar.A implementação do GHC
putStrLn
funciona copiando os caracteres do argumento String para um buffer de saída. Mas quando ele entra neste loop,show
ainda não foi executado. Portanto, quando vai copiar o primeiro caractere da string, Haskell avalia a fração deshow
e asquicksort
chamadas necessárias para calcular esse caractere . Em seguida,putStrLn
passa para o próximo personagem. Portanto, a execução de todas as três funçõesputStrLn
-show
, equicksort
- é intercalada.quicksort
executa de forma incremental, deixando um gráfico de thunks não avaliados à medida que vai para lembrar onde parou.Agora, isso é totalmente diferente do que você pode esperar se estiver familiarizado com, você sabe, qualquer outra linguagem de programação. Não é fácil visualizar como
quicksort
realmente se comporta em Haskell em termos de acessos à memória ou mesmo a ordem das comparações. Se você pudesse apenas observar o comportamento, e não o código-fonte, não reconheceria o que ele está fazendo como um quicksort .Por exemplo, a versão C do quicksort particiona todos os dados antes da primeira chamada recursiva. Na versão Haskell, o primeiro elemento do resultado será calculado (e pode até aparecer em sua tela) antes que a execução da primeira partição termine - na verdade, antes que qualquer trabalho seja concluído
greater
.PS O código Haskell seria mais semelhante a quicksort se fizesse o mesmo número de comparações que quicksort; o código conforme escrito faz o dobro de comparações porque
lesser
egreater
são especificados para serem calculados independentemente, fazendo duas varreduras lineares na lista. É claro que, em princípio, é possível que o compilador seja inteligente o suficiente para eliminar as comparações extras; ou o código pode ser alterado para usoData.List.partition
.PPS O exemplo clássico de algoritmos Haskell que não se comportam como você esperava é a peneira de Eratóstenes para computar os primos.
fonte
primes = unfoldr (\(p:xs)-> Just (p, filter ((> 0).(`rem` p)) xs)) [2..]
, seu problema mais imediato seria talvez mais claro. E isso antes de considerarmos a mudança para o algoritmo de peneira verdadeiro.putStrLn
chamasse um aplicativo convertido deshow
para um aplicativo convertido dequicksort
para uma lista literal --- e é exatamente isso que ele faz! (antes da otimização --- mas compare o código C com o montador otimizado algum dia!). Talvez você queira dizer "graças à avaliação preguiçosa, um programa Haskell não faz o que um código de aparência semelhante faz em outras linguagens"?Eu acredito que a razão pela qual a maioria das pessoas diz que o bonito Haskell Quicksort não é um "verdadeiro" Quicksort é o fato de que ele não está no lugar - claramente, não pode ser quando se usa tipos de dados imutáveis. Mas também há a objeção de que não é "rápido": em parte por causa do ++ caro, e também porque há um vazamento de espaço - você se agarra à lista de entrada enquanto faz a chamada recursiva nos elementos menores, e em alguns casos - por exemplo, quando a lista está diminuindo - isso resulta em uso de espaço quadrático. (Você pode dizer que fazê-lo funcionar no espaço linear é o mais próximo que você pode chegar do "local" usando dados imutáveis.) Existem soluções simples para ambos os problemas, usando parâmetros de acumulação, tuplagem e fusão; consulte S7.6.1 de Richard Bird '
fonte
Não é a ideia de elementos mutantes no local em ambientes puramente funcionais. Os métodos alternativos neste segmento com matrizes mutáveis perderam o espírito de pureza.
Existem pelo menos duas etapas para otimizar a versão básica (que é a versão mais expressiva) de classificação rápida.
Otimize a concatenação (++), que é uma operação linear, por acumuladores:
Otimize para classificação rápida ternária (partição de 3 vias, mencionada por Bentley e Sedgewick), para lidar com elementos duplicados:
Combine 2 e 3, consulte o livro de Richard Bird:
Ou, alternativamente, se os elementos duplicados não forem a maioria:
Infelizmente, mediana de três não pode ser implementada com o mesmo efeito, por exemplo:
porque ainda tem um desempenho insatisfatório nos 4 casos a seguir:
[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 esses 4 casos são bem tratados pela abordagem imperativa de mediana de três.
Na verdade, o algoritmo de classificação mais adequado para uma configuração puramente funcional ainda é a classificação por mesclagem, mas não a classificação rápida.
Para obter detalhes, visite minha redação contínua em: https://sites.google.com/site/algoxy/dcsort
fonte
Não há uma definição clara do que é e do que não é um verdadeiro quicksort.
Eles estão chamando de não uma classificação rápida verdadeira, porque não classifica no local:
fonte
Porque tirar o primeiro elemento da lista resulta em um tempo de execução muito ruim. Use a mediana de 3: primeiro, meio, último.
fonte
O(n^2)
Peça a qualquer pessoa para escrever quicksort em Haskell, e você obterá essencialmente o mesmo programa - é obviamente um quicksort. Aqui estão algumas vantagens e desvantagens:
Pro: melhora a classificação rápida "verdadeira" por ser estável, ou seja, preserva a ordem da sequência entre elementos iguais.
Pro: É trivial generalizar para uma divisão de três vias (<=>), o que evita o comportamento quadrático devido a algum valor ocorrer O (n) vezes.
Pro: é mais fácil de ler - mesmo que fosse necessário incluir a definição de filtro.
Contra: usa mais memória.
Contra: É caro generalizar a escolha do pivô por amostragem adicional, o que poderia evitar o comportamento quadrático em certas ordenações de baixa entropia.
fonte