Auf der Haskell-Website wird eine sehr attraktive 5-Zeilen- Quicksort-Funktion vorgestellt (siehe unten).
quicksort [] = []
quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
where
lesser = filter (< p) xs
greater = filter (>= p) xs
Sie enthalten auch eine "True Quicksort in 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 );
}
}
Ein Link unter der C-Version verweist auf eine Seite mit der Angabe "Der in Einführung zitierte Quicksort ist nicht der" echte "Quicksort und lässt sich nicht wie der C-Code für längere Listen skalieren."
Warum ist die obige Haskell-Funktion keine echte Quicksortierung? Wie kann es nicht für längere Listen skaliert werden?
O(N^2)
Laufzeit hat.Antworten:
Die wahre Quicksort hat zwei schöne Aspekte:
Das kurze Haskell-Beispiel zeigt (1), aber nicht (2). Wie (2) gemacht wird, ist möglicherweise nicht offensichtlich, wenn Sie die Technik noch nicht kennen!
quelle
Echte Inplace-Quicksort in Haskell:
quelle
unstablePartition
ist sehr ähnlich zupartition
forquicksort
, garantiert jedoch nicht, dass das Element anm
der Position gerecht istp
.Hier ist eine Transliteration des "wahren" Quicksort-C-Codes in Haskell. Mach dich bereit.
Das hat Spaß gemacht, nicht wahr? Ich habe diese Größe sowohl
let
am Anfang als auchwhere
am Ende der Funktion ausgeschnitten und alle Helfer definiert, um den vorhergehenden Code etwas hübsch zu machen.Und hier ein dummer Test, um zu sehen, ob es funktioniert.
Ich schreibe nicht sehr oft imperativen Code in Haskell, daher gibt es sicher viele Möglichkeiten, diesen Code zu bereinigen.
Na und?
Sie werden feststellen, dass der obige Code sehr, sehr lang ist. Das Herzstück ist ungefähr so lang wie der C-Code, obwohl jede Zeile oft etwas ausführlicher ist. Dies liegt daran, dass C heimlich viele böse Dinge tut, die Sie für selbstverständlich halten könnten. Zum Beispiel
a[l] = a[h];
. Dies greift auf die veränderlichen Variablenl
und zuh
und greift dann auf das veränderbare Array zua
und mutiert dann das veränderbare Arraya
. Heilige Mutation, Batman! In Haskell ist die Mutation und der Zugriff auf veränderbare Variablen explizit. Das "gefälschte" Qsort ist aus verschiedenen Gründen attraktiv, aber das Wichtigste unter ihnen ist, dass es keine Mutation verwendet. Diese selbst auferlegte Einschränkung erleichtert das Verständnis auf einen Blick.quelle
Meiner Meinung nach übertreibt die Aussage, dass es sich nicht um eine echte Quicksortierung handelt, den Fall. Ich denke, es ist eine gültige Implementierung des Quicksort-Algorithmus , nur keine besonders effiziente.
quelle
Ich denke, der Fall, den dieses Argument anstrebt, ist, dass der Grund, warum Quicksort häufig verwendet wird, darin besteht, dass es vorhanden und daher ziemlich cachefreundlich ist. Da Sie diese Vorteile bei Haskell-Listen nicht haben, ist die Hauptaufgabe weg, und Sie können auch die Zusammenführungssortierung verwenden, die O (n log n) garantiert , während Sie bei Quicksort entweder Randomisierung oder Kompliziertheit verwenden müssen Partitionierungsschemata zur Vermeidung der Laufzeit von O (n 2 ) im schlimmsten Fall.
quelle
Dank der verzögerten Auswertung kann ein Haskell-Programm nicht (fast nicht ) das tun, wie es aussieht.
Betrachten Sie dieses Programm:
In einer eifrigen Sprache
quicksort
würde dann zuerst laufenshow
, dannputStrLn
. Die Argumente einer Funktion werden berechnet, bevor diese Funktion ausgeführt wird.In Haskell ist es umgekehrt. Die Funktion wird zuerst ausgeführt. Die Argumente werden nur berechnet, wenn die Funktion sie tatsächlich verwendet. Und ein zusammengesetztes Argument wird wie eine Liste Stück für Stück berechnet, da jedes Stück davon verwendet wird.
Das erste , was in diesem Programm passiert, ist, dass es gestartet wird
putStrLn
.Die Implementierung von GHC
putStrLn
funktioniert durch Kopieren der Zeichen des Arguments String in einen Ausgabepuffer. Aber wenn es in diese Schleife eintritt,show
ist es noch nicht gelaufen. Wenn das erste Zeichen aus der Zeichenfolge kopiert wird, wertet Haskell daher den Bruchteil dershow
undquicksort
Aufrufe aus, die zur Berechnung dieses Zeichens erforderlich sind . DannputStrLn
bewegt sich auf das nächste Zeichen. Die Ausführung aller drei FunktionenputStrLn
-show
, undquicksort
- ist also verschachtelt.quicksort
wird schrittweise ausgeführt und hinterlässt ein Diagramm mit nicht bewerteten Thunks, um sich daran zu erinnern, wo es aufgehört hat.Dies unterscheidet sich grundlegend von dem, was Sie erwarten könnten, wenn Sie mit einer anderen Programmiersprache vertraut sind. Es ist nicht einfach zu visualisieren, wie
quicksort
sich Haskell in Bezug auf Speicherzugriffe oder sogar die Reihenfolge der Vergleiche tatsächlich verhält. Wenn Sie nur das Verhalten und nicht den Quellcode beobachten könnten, würden Sie nicht erkennen, was es als Quicksort tut .Beispielsweise partitioniert die C-Version von Quicksort alle Daten vor dem ersten rekursiven Aufruf. In der Haskell-Version wird das erste Element des Ergebnisses berechnet (und kann sogar auf Ihrem Bildschirm angezeigt werden), bevor die erste Partition ausgeführt wird - tatsächlich bevor überhaupt daran gearbeitet wird
greater
.PS Der Haskell-Code wäre schneller, wenn er die gleiche Anzahl von Vergleichen wie Quicksort durchführen würde. der Code geschrieben hat doppelt so viele Vergleiche da
lesser
undgreater
spezifiziert unabhängig berechnet werden, zwei lineare Scans durch die Liste zu tun. Natürlich ist es im Prinzip möglich, dass der Compiler intelligent genug ist, um die zusätzlichen Vergleiche zu eliminieren. oder der Code könnte geändert werden, um zu verwendenData.List.partition
.PPS Das klassische Beispiel für Haskell-Algorithmen, die sich nicht so verhalten, wie Sie es erwartet haben, ist das Eratosthenes-Sieb für die Berechnung von Primzahlen.
quelle
primes = unfoldr (\(p:xs)-> Just (p, filter ((> 0).(`rem` p)) xs)) [2..]
, seine unmittelbarste Problem wäre vielleicht klarer. Und das ist, bevor wir überlegen, auf den echten Siebalgorithmus umzusteigen.putStrLn
eine Thunked-Anwendungshow
auf eine Thunked-Anwendung auf ein Listenliteral aufrufenquicksort
- und genau das tut er! (vor der Optimierung --- aber vergleichen Sie den C-Code irgendwann mit dem optimierten Assembler!). Vielleicht meinen Sie "dank der verzögerten Auswertung macht ein Haskell-Programm nicht das, was ähnlich aussehender Code in anderen Sprachen macht"?Ich glaube, der Grund, warum die meisten Leute sagen, dass das hübsche Haskell Quicksort kein "echtes" Quicksort ist, ist die Tatsache, dass es nicht vorhanden ist - klar, es kann nicht sein, wenn unveränderliche Datentypen verwendet werden. Es gibt aber auch den Einwand, dass es nicht "schnell" ist: teilweise wegen des teuren ++ und auch wegen eines Speicherplatzlecks - Sie halten an der Eingabeliste fest, während Sie den rekursiven Aufruf für die kleineren Elemente ausführen, und In einigen Fällen - z. B. wenn die Liste abnimmt - führt dies zu einer quadratischen Raumnutzung. (Man könnte sagen, dass es im linearen Raum am ehesten "an Ort und Stelle" ist, wenn unveränderliche Daten verwendet werden.) Für beide Probleme gibt es gute Lösungen: Akkumulieren von Parametern, Tupeln und Verschmelzen. siehe S7.6.1 von Richard Bird '
quelle
Es ist nicht die Idee, Elemente in rein funktionalen Umgebungen zu mutieren. Die alternativen Methoden in diesem Thread mit veränderlichen Arrays haben den Geist der Reinheit verloren.
Es gibt mindestens zwei Schritte, um die Basisversion (die ausdrucksstärkste Version) der Schnellsortierung zu optimieren.
Optimieren Sie die Verkettung (++), eine lineare Operation, durch Akkumulatoren:
Optimieren Sie auf ternäre schnelle Sortierung (3-Wege-Partition, von Bentley und Sedgewick erwähnt), um doppelte Elemente zu verarbeiten:
Kombinieren Sie 2 und 3, siehe Richard Birds Buch:
Oder alternativ, wenn die duplizierten Elemente nicht die Mehrheit sind:
Leider kann der Median-of-Three nicht mit demselben Effekt implementiert werden, zum Beispiel:
weil es in den folgenden 4 Fällen immer noch schlecht abschneidet:
[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, ...]
Alle diese 4 Fälle werden durch den imperativen Median-of-Three-Ansatz gut behandelt.
Tatsächlich ist der am besten geeignete Sortieralgorithmus für eine rein funktionale Einstellung immer noch das Zusammenführen, aber nicht das schnelle Sortieren.
Weitere Informationen finden Sie in meinem laufenden Artikel unter: https://sites.google.com/site/algoxy/dcsort
quelle
Es gibt keine klare Definition dessen, was ein echter Quicksort ist und was nicht.
Sie nennen es keine echte Quicksortierung, weil es nicht an Ort und Stelle sortiert wird:
quelle
Weil das erste Element aus der Liste zu einer sehr schlechten Laufzeit führt. Verwenden Sie den Median 3: zuerst, mittel, zuletzt.
quelle
O(n^2)
Wenn Sie jemanden bitten, in Haskell Quicksort zu schreiben, erhalten Sie im Wesentlichen das gleiche Programm - es ist offensichtlich Quicksort. Hier einige Vor- und Nachteile:
Pro: Es verbessert die "echte" Quicksortierung, indem es stabil ist, dh es behält die Sequenzreihenfolge zwischen gleichen Elementen bei.
Pro: Es ist trivial, auf eine Drei-Wege-Aufteilung (<=>) zu verallgemeinern, die ein quadratisches Verhalten aufgrund eines Wertes vermeidet, der O (n) mal auftritt.
Pro: Es ist einfacher zu lesen - auch wenn man die Definition des Filters einschließen musste.
Con: Es verwendet mehr Speicher.
Con: Es ist kostspielig, die Auswahl des Pivots durch weitere Abtastung zu verallgemeinern, wodurch ein quadratisches Verhalten bei bestimmten Ordnungen mit niedriger Entropie vermieden werden könnte.
quelle