Folgender SML-Code implementiert den QuickSort streng nach Definition —
der
QuickSort in Reinkultur!
Zudem besitzt speziell dieser SML-QuickSort folgende Eigenschaften:
- Glattheit
Beispielsweise werden Listen mit gleichwertigen Elementen in
O(n)
Schritten sortiert.
Die Rekursion käme praktisch nicht zustande,
weil schon im ersten
(Rekursions-)
Aufruf alle Elemente der zu sortierenden Liste in die mittlere Liste
medium
aufgenommen werden würden, die beiden anderen Listen
small
und
large
hingegen leer blieben.
- Stabilität
Die Reihenfolge gleichwertiger Elemente wird stets
aufrechterhalten
(siehe Filter-Funktion).
Dabei ist aber der QuickSort als nicht stabiler Sortieralgorithmus bekannt.
Man spricht allerdings indirekt von der
„In-Placement-Version”
des QuickSort.
Diese SML-Implementierung zeigt einen stabilen QuickSort!
Mehr zu diesen Eigenschaften finden Sie unter
Sortieren.
Aus dieser Implementierung geht deutlich hervor,
dass der QuickSort ein
Divide-and-Conquer - Algorithmus
ist und mit einer kaskadenartigen bzw. baumartigen Rekursion realisiert werden muss.
Diese Rekursion erinnert stark an eine
Inorder-Traversion
eines binären Baumes.
Deshalb spricht man auch häufig davon, dass der QuickSort eine Baumstruktur aufbaut.
Der einzelne Knoten des Binärbaumes wäre demnach 3-teilig:
- Links
(hier: quicksort small)
Der Verweis auf einen weiteren Knoten des Baumes bzw. auf die sortierte Teilliste aller
kleineren
Elemente bzgl. des Pivotelements.
- Mitte
(hier: medium)
Die Liste aller Elemente, die dem Pivotelement
gleichwertig
sind, inklusive des Pivotelements selbst.
Dieser Mitte verdankt man übrigens ein deutlich besseres Laufzeitverhalten,
wenn Listen mit verhältnismäßig vielen gleichwertigen Elementen sortiert werden
(Glattheit).
- Rechts
(hier: quicksort large)
Der Verweis auf einen weiteren Knoten des Baumes bzw. auf die sortierte Teilliste aller
größeren
Elemente bzgl. des Pivotelements.
(* filter: Hilfsfunktion
gibt nur die Elemente zurück,
die den Vergleich mittels elem und cmp bestehen.
Die Reihenfolge dieser Elemente bleibt erhalten bzw. stabil! *)
fun filter nil elem cmp = nil
| filter (x::xl) elem cmp =
if (cmp(x, elem))
then x :: filter xl elem cmp
else filter xl elem cmp;
(* quicksort: klar!
small: Elemente, die kleiner sind als das Pivotelement.
medium: Pivotelement mit den Elementen, die dem Pivotelement gleichen.
large: Elemente, die größer sind als das Pivotelement. *)
fun quicksort nil = nil
| quicksort (pivot::xl) =
let
val small = filter xl pivot (op <);
val medium = pivot :: filter xl pivot (op =);
val large = filter xl pivot (op >);
in
(quicksort small) @ medium @ (quicksort large)
end;
(* Anwendungsbeispiel *)
val output1 = quicksort [3, 6, 2, 9, 1, 8, 7, 4, 5];
val output2 = quicksort [7, 7, 7, 2, 7, 2, 2, 2];
val output3 = quicksort [~3, 3, ~2, 2];
(* Ausgabe:
val output1 = [1,2,3,4,5,6,7,8,9] : int list
val output2 = [2,2,2,2,7,7,7,7] : int list
val output3 = [~3,~2,2,3] : int list *)
Die Prolog-Implementierung des QuickSort ist der vorhergehenden SML-Implementierung sehr ähnlich,
nur dass dieser Algorithmus die Glattheit nicht berücksichtigt,
weil hier die zu sortierende Liste nur in zwei kleinere Listen aufgeteilt wird.
Die mittlere Liste gibt es in diesem Beispiel nicht, sie besteht praktisch nur aus dem Pivotelement selbst.
% ?-quicksort([5, 2, 7, 4, 9, 6, 2, 1, 5, 3, 8], SL).
quicksort([], []).
quicksort([Pivot|L], SL) :-
divide(Pivot, L, L_Lower, L_Larger),
quicksort(L_Lower, SL_Lower),
quicksort(L_Larger, SL_Larger),
append(SL_Lower, [Pivot|SL_Larger], SL).
% divide: Liste wird in zwei Listen [ < X ] und [ >= X ] zerlegt.
divide(_, [], [], []).
divide(X, [Y|L], [Y|L_Lower], L_Larger) :-
X > Y,
!,
divide(X, L, L_Lower, L_Larger).
divide(X, [Y|L], L_Lower, [Y|L_Larger]) :-
divide(X, L, L_Lower, L_Larger).
Der Schreibaufwand mit Haskell ist deutlich kleiner als bei der entsprechenden
Prolog-Implementierung.
Auch hier wird die Glattheit nicht berücksichtigt.
Die Elemente, die dem Pivotelement
p
gleichen, werden mit den Elementen vermengt, welche größer
(g)
als das Pivotelement sind.
Möglicherweise gibt es keine andere Sprache, in der der QuickSort noch kürzer implementiert werden kann.
quicksort :: Ord a => [a] -> [a]
quicksort [] = []
quicksort (p:r) = quicksort[k|k<-r,k<p] ++ [p] ++ quicksort[g|g<-r,g>=p]
Im Folgenden wird der QuickSort als
In-Placement-Sortierverfahren
implementiert, was einen höheren und komplizierten Programmieraufwand zur Folge hat.
Um die Ordnung einer Teilliste bzgl. des Pivotelements herzustellen,
benötigt man lediglich so viel Speicherplatz, wie die kaskadenartige Rekursion an Stack benötigt.
Um
in-place
sortieren zu können, geht man meist zwei Kompromisse ein:
- Verlust der Glattheit!
Man teilt die Liste bei jedem Rekursionsschritt nur noch in zwei Teile:
Die Elemente, die dem Pivotelement gleichwertig sind,
werden je nach Art der Implementierung in die linke bzw. in die rechte Teilliste geswappt;
die mittlere Liste besteht praktisch nur noch aus einem einzigen Element, dem Pivotelement selbst.
Aus diesem Grunde werden selbst die Elemente, die dem Pivotelement gleichwertig sind, nochmals
(überflüssigerweise)
in den folgenden Rekursionsschritten betrachtet, bis diese selbst zu Pivotelementen werden
oder bis der triviale Fall
(Terminierung)
eintritt.
- Verlust der Stabilität!
Elemente, die dem Pivotelement gleichwertig sind, werden genauso wie die anderen Elemente behandelt.
Durch das permanente Vertauschen der Elemente innerhalb der zu sortierenden Liste kann die Reihenfolge gleichwertiger Elemente
nicht mehr nachvollzogen werden.
Wie ein In-Placement-QuickSort zu einem
glatten
Sortierverfahren modifiziert werden kann, wird beispielsweise recht ausführlich in
[ALGODAT S. 96f]
beschrieben.
Jedoch geht die Stabilität beim In-Placement-QuickSort für immer verloren.
Bei Wunsch nach Stabilität sollte man auf andere stabile Sortierverfahren zurückgreifen,
wie zum Beispiel auf eine stabile Implementierung des
HeapSort.
unit UQuickSort;
interface
procedure QuickSort(var A: array of Integer);
implementation
procedure QuickSort(var A: array of Integer);
procedure QSort(LoIndex, HiIndex: Integer);
var
Lo, Hi: Integer;
Pivot: Integer;
Swap: Integer;
begin
// Wähle stets das mittlere Element als Pivotelement.
Pivot := A[(LoIndex + HiIndex) div 2];
// Stelle die Ordnung bzgl. des Pivotelements her.
Lo := LoIndex;
Hi := HiIndex;
repeat
while A[Lo] < Pivot do Inc(Lo);
while A[Hi] > Pivot do Dec(Hi);
if Lo <= Hi then
begin
Swap := A[Lo];
A[Lo] := A[Hi];
A[Hi] := Swap;
Inc(Lo);
Dec(Hi);
end;
until Lo > Hi;
// Gegebenenfalls linke Teilliste sortieren.
if LoIndex < Hi then QSort(LoIndex, Hi);
// Gegebenenfalls rechte Teilliste sortieren.
if Lo < HiIndex then QSort(Lo, HiIndex);
end;
begin
QSort(Low(A), High(A));
end;
end.
Da in der hier gezeigten Delphi-Implementierung des QuickSort
stets das mittlere Element einer Teilliste als Pivotelement ausgezeichnet wird,
entsteht eine unnötige Geschwindigkeitsbremse, nämlich die Berechnung der Indexposition des Pivots:
(LoIndex + HiIndex) div 2.
Wählt man stets das mittlere Element als Pivotelement, geht man einer
Worst-Case-Laufzeitkomplexität
nicht aus dem Wege
(siehe oben, das dritte konkrete Zahlenbeispiel),
schlimmer noch: Der QuickSort büßt an Effizienz ein,
wo doch gerade die Effizienz die Stärke des QuickSort darstellen sollte.
Im folgenden
Code-Beispiel
in
C++
wird stets das am weitesten rechts liegende Randelement als Pivotelement ausgezeichnet
— es besteht kein besonderer Anlass mehr, das Pivotelement teuer zu ermitteln.
Dies soll aber nicht bedeuten, dass C++ schneller ist als Delphi,
sondern nur, dass das folgende Beispiel einen effizienteren Algorithmus implementiert (algorithmusabhängig, nicht programmiersprachenabhängig).
#include <stdlib.h>
#include <stdio.h>
void quicksort(int *a, int left, int right) {
/* Betrachtung des (Teil-)Arrays,
* das mindestens zwei Elemente umfasst,
* left und right sind die Grenzen des (Teil-)Arrays. */
if (left < right) {
int pivot = a[right]; // das rechte Grenzelement, Pivotelement!
int l = left;
int r = right;
do {
/* Den linken Index so weit nach rechts rücken (erhöhen),
* solange das l-Element kleiner ist als das Pivotelement. */
while (a[l] < pivot) l++;
/* Den rechten Index so weit nach links rücken (verkleinern),
* solange das r-Element größer ist als das Pivotelement. */
while (a[r] > pivot) r--;
/* Vertausche die aktuellen r- und l-Elemente und
* erhöhe l und verringere r jeweils um 1,
* falls l noch links von r (rechts) ist! */
if (l <= r) {
int swap = a[l];
a[l] = a[r];
a[r] = swap;
l++;
r--;
}
} while (l <= r); // bis sich l und r irgendwo in der Mitte treffen.
quicksort(a, left, r); // sortiere die linke Teilliste.
quicksort(a, l, right); // sortiere die rechte Teilliste.
}
}
// Kleiner Test: Lass quicken!
int main() {
int values[] = {3, 9, 2, 0, 5, 18, 11, 4, 77, 16};
int count = sizeof(values) / sizeof(*values);
// Ausgabe der Werte (vorher)
for (int i = 0; i < count; i++) printf("%5d", values[i]);
printf("\n");
// Sortiere alle Elemente von 0 bis count-1!
quicksort(values, 0, count - 1);
// Ausgabe der Werte (nachher)
for (int i = 0; i < count; i++) printf("%5d", values[i]);
printf("\n");
system("PAUSE");
return 0;
}
Dank
Christian Weßel
kann ich Ihnen auch einen QuickSort in Perl präsentieren, sehen Sie selbst!
#!/usr/bin/perl
sub QuickSort {
my ($comp,$left,$right,@list) = @_;
if ($left < $right) {
$pivot = $list[$right];
$l = $left;
$r = $right;
do {
while (&$comp($list[$l], $pivot) && $l<$right) {$l++};
while (&$comp($pivot, $list[$r]) && $r>$left) {$r--};
if ($l <= $r) {
($list[$l], $list[$r]) = ($list[$r], $list[$l]);
$l++;
$r--;
};
} while ($l <= $r);
@list = &QuickSort($comp, $left, $r, @list);
@list = &QuickSort($comp, $l, $right, @list);
};
return @list;
};
# --- Anwendungsbeispiel ---
# Vergleichsfunktionen
my $numAsc = sub {return $_[0] < $_[1];};
my $numDesc = sub {return $_[0] > $_[1];};
@A = (4,2,9,3,0,5,1,8,6,7);
@B = &QuickSort($numAsc, 0, $#A, @A);
@C = &QuickSort($numDesc, 0, $#A, @A);
print "# @A (unsortiert)\n";
print "#\n";
print "# @B (aufsteigend; asc)\n";
print "# @C (absteigend; desc)\n";
exit;