Witryna Haskell wprowadza bardzo atrakcyjną 5-wierszową funkcję szybkiego sortowania , jak widać poniżej.
quicksort [] = []
quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
where
lesser = filter (< p) xs
greater = filter (>= p) xs
Obejmują również „Prawdziwe szybkie sortowanie w 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 );
}
}
Odnośnik pod wersją C kieruje do strony, na której stwierdza się: „Quicksort cytowany we wstępie nie jest„ prawdziwym ”quicksort i nie skaluje się dla dłuższych list, jak robi to kod c.
Dlaczego powyższa funkcja Haskell nie jest prawdziwym szybkim sortowaniem? Jak to się nie udaje przy dłuższych listach?
O(N^2)
środowisko uruchomieniowe .Odpowiedzi:
Prawdziwy quicksort ma dwa piękne aspekty:
Krótki przykład Haskella pokazuje (1), ale nie (2). Sposób wykonania (2) może nie być oczywisty, jeśli nie znasz jeszcze techniki!
źródło
Prawdziwe szybkie sortowanie w miejscu w Haskell:
źródło
unstablePartition
jest bardzo podobny dopartition
forquicksort
, ale nie gwarantuje, że element nam
tej pozycji jest sprawiedliwyp
.Oto transliteracja „prawdziwego” kodu szybkiego sortowania C na Haskell. Przygotuj się.
To było zabawne, prawda? Właściwie wyciąłem ten duży
let
na początku, a także plikwhere
na końcu funkcji, definiując wszystkich pomocników, aby poprzedni kod był nieco ładny.A tutaj głupi test, aby sprawdzić, czy to działa.
Nie piszę zbyt często kodu imperatywnego w Haskell, więc jestem pewien, że jest wiele sposobów na wyczyszczenie tego kodu.
Więc co?
Zauważysz, że powyższy kod jest bardzo, bardzo długi. Jej sercem jest prawie tak długi, jak kod C, chociaż każda linia jest często nieco bardziej szczegółowa. Dzieje się tak, ponieważ C potajemnie robi wiele paskudnych rzeczy, które możesz wziąć za pewnik. Na przykład
a[l] = a[h];
. Ten dostęp zmienne zmiennyl
ih
, a następnie uzyskuje dostęp do tablicy zmiennya
, a następnie mutuje się zmienny układa
. Święta mutacja, batmanie! W Haskell mutacja i dostęp do zmiennych zmiennych są jawne. „Fałszywy” qsort jest atrakcyjny z różnych powodów, ale głównym z nich jest to, że nie wykorzystuje mutacji; to samo narzucone ograniczenie znacznie ułatwia zrozumienie na pierwszy rzut oka.źródło
Moim zdaniem stwierdzenie, że to „nie jest prawdziwy szybki przegląd”, przesadza. Myślę, że jest to poprawna implementacja algorytmu Quicksort , ale nie jest to szczególnie wydajna.
źródło
Myślę, że argument, który próbuje przedstawić ten argument, polega na tym, że powodem, dla którego często używa się quicksort, jest to, że jest on na miejscu i w rezultacie dość przyjazny dla pamięci podręcznej. Ponieważ nie masz tych korzyści z listami Haskella, jego główna racja bytu zniknęła i równie dobrze możesz użyć sortowania przez scalanie, które gwarantuje O (n log n) , podczas gdy w przypadku szybkiego sortowania musisz użyć randomizacji lub skomplikowanego schematy partycjonowania w celu uniknięcia czasu wykonywania O (n 2 ) w najgorszym przypadku.
źródło
Dzięki leniwej ocenie program Haskell nie (prawie nie może ) robić tego, na co wygląda.
Rozważ ten program:
W gorliwym języku najpierw
quicksort
biegałshow
, potemputStrLn
. Argumenty funkcji są obliczane przed uruchomieniem tej funkcji.W Haskell jest odwrotnie. Funkcja jest uruchamiana jako pierwsza. Argumenty są obliczane tylko wtedy, gdy funkcja faktycznie ich używa. Argument złożony, taki jak lista, jest obliczany po jednym fragmencie, gdy jest używany.
Więc pierwszą rzeczą, która dzieje się w tym programie, jest to, że
putStrLn
zaczyna działać.Implementacja GHC
putStrLn
działa poprzez kopiowanie znaków argumentu String do bufora wyjściowego. Ale kiedy wchodzi w tę pętlę,show
jeszcze nie działa. Dlatego też, kiedy zamierza skopiować pierwszy znak z ciągu, Haskell ocenia ułamek wywołańshow
iquicksort
potrzebnych do obliczenia tego znaku . NastępnieputStrLn
przechodzi do następnego znaku. Zatem wykonanie wszystkich trzech funkcjiputStrLn
-show
, iquicksort
- jest przeplatane.quicksort
wykonuje się przyrostowo, pozostawiając wykres nieocenionych fragmentów w miarę zapamiętywania miejsca, w którym zostało przerwane.To bardzo różni się od tego, czego można by się spodziewać, jeśli znasz, wiesz, jakikolwiek inny język programowania. Nie jest łatwo wyobrazić sobie, jak
quicksort
faktycznie zachowuje się Haskell pod względem dostępu do pamięci, a nawet kolejności porównań. Gdybyś mógł tylko obserwować zachowanie, a nie kod źródłowy, nie rozpoznałbyś tego, co robi jako szybki sort .Na przykład wersja C quicksort dzieli na partycje wszystkie dane przed pierwszym wywołaniem rekurencyjnym. W wersji Haskell, pierwszy element wyniku zostanie obliczony (i może nawet pojawić się na ekranie) przed zakończeniem działania pierwszej partycji - a właściwie przed wykonaniem jakiejkolwiek pracy
greater
.PS Kod Haskell byłby bardziej podobny do szybkiego sortowania, gdyby wykonywał taką samą liczbę porównań jak w przypadku szybkiego sortowania; napisany kod wykonuje dwa razy więcej porównań, ponieważ
lesser
igreater
są określone do obliczania niezależnie, wykonując dwa liniowe skanowanie listy. Oczywiście w zasadzie kompilator może być wystarczająco inteligentny, aby wyeliminować dodatkowe porównania; lub kod można zmienić do użyciaData.List.partition
.PPS Klasycznym przykładem algorytmów Haskella, które okazały się nie zachowywać się zgodnie z oczekiwaniami, jest sito Eratostenesa do obliczania liczb pierwszych.
źródło
primes = unfoldr (\(p:xs)-> Just (p, filter ((> 0).(`rem` p)) xs)) [2..]
, jej najpilniejszy problem byłby być może jaśniejszy. I to zanim rozważymy przejście na algorytm prawdziwego sita.putStrLn
której aplikacjishow
zmyślonej, lub z aplikacji,quicksort
do listy - i to jest dokładnie to, co robi! (przed optymalizacją --- ale czasami porównaj kod C ze zoptymalizowanym asemblerem!). Może masz na myśli „dzięki leniwej ocenie, program Haskell nie robi tego, co podobnie wyglądający kod robi w innych językach”?Uważam, że powodem, dla którego większość ludzi twierdzi, że ładny Haskell Quicksort nie jest „prawdziwym” Quicksort, jest fakt, że nie jest on na miejscu - oczywiście nie może tak być, gdy używa się niezmiennych typów danych. Ale jest też zastrzeżenie, że nie jest to „szybkie”: częściowo z powodu drogiego ++, a także z powodu wycieku spacji - trzymasz się listy wejściowej podczas wykonywania rekurencyjnego wywołania mniejszych elementów i w niektórych przypadkach - np. gdy lista jest malejąca - powoduje to kwadratowe wykorzystanie przestrzeni. (Można powiedzieć, że sprawienie, by działał w przestrzeni liniowej, jest najbliższym osiągnięciem „w miejscu” przy użyciu niezmiennych danych.) Istnieją zgrabne rozwiązania obu problemów, wykorzystując akumulację parametrów, kręcenie i fuzję; patrz S7.6.1 książki Richard Bird '
źródło
To nie jest idea mutowania elementów na miejscu w czysto funkcjonalnych warunkach. Alternatywne metody w tym wątku z zmiennymi tablicami straciły ducha czystości.
Istnieją co najmniej dwa kroki, aby zoptymalizować podstawową wersję (która jest najbardziej wyrazistą wersją) szybkiego sortowania.
Zoptymalizuj konkatenację (++), która jest operacją liniową, przez akumulatory:
Zoptymalizuj do trójskładnikowego szybkiego sortowania (trójdrożna partycja, o której wspominają Bentley i Sedgewick), aby obsługiwać zduplikowane elementy:
Połącz 2 i 3, patrz książka Richarda Birda:
Lub alternatywnie, jeśli zduplikowane elementy nie stanowią większości:
Niestety, mediana-trzech nie może zostać zaimplementowana z takim samym skutkiem, na przykład:
ponieważ nadal działa słabo w następujących 4 przypadkach:
[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, ...]
Wszystkie te 4 przypadki są dobrze obsługiwane przez imperatywną metodę mediany z trzech.
W rzeczywistości najbardziej odpowiednim algorytmem sortowania dla czysto funkcjonalnego ustawienia jest nadal sortowanie przez scalanie, ale nie sortowanie szybkie.
Aby uzyskać szczegółowe informacje, zapoznaj się z moim ciągłym pisaniem pod adresem : https://sites.google.com/site/algoxy/dcsort
źródło
Nie ma jasnej definicji tego, co jest, a co nie jest prawdziwym szybkim sortowaniem.
Nazywają to nie prawdziwym szybkim sortowaniem, ponieważ nie sortuje na miejscu:
źródło
Ponieważ pobranie pierwszego elementu z listy skutkuje bardzo złym działaniem. Użyj mediany 3: pierwsza, środkowa, ostatnia.
źródło
O(n^2)
Poproś kogokolwiek o napisanie quicksort w Haskell, a otrzymasz zasadniczo ten sam program - jest to oczywiście quicksort. Oto kilka zalet i wad:
Pro: Poprawia "prawdziwego" szybkiego sortowania poprzez bycie stabilnym, tj. Zachowuje kolejność między równymi elementami.
Zaleta: Uogólnienie do podziału na trzy części (<=>), który pozwala uniknąć zachowania kwadratowego z powodu pewnej wartości występującej O (n) razy, jest trywialne.
Pro: Jest łatwiejszy do odczytania - nawet gdyby trzeba było uwzględnić definicję filtra.
Wada: zużywa więcej pamięci.
Wada: Kosztowne jest uogólnianie wyboru obrotu przez dalsze próbkowanie, co mogłoby uniknąć zachowania kwadratowego na niektórych uporządkowaniach o niskiej entropii.
źródło