„Radixsort“ – Versionsunterschied
[gesichtete Version] | [gesichtete Version] |
K →Vorgehensweise (mittels Listen): Anker |
→Vorgehensweise (in-place): Nacharbeit angefordert |
||
Zeile 30: | Zeile 30: | ||
== Vorgehensweise (in-place) == |
== Vorgehensweise (in-place) == |
||
{{Vorlage:Verbessern}}{{Kommentar}}: Die Funktionsweise ist unvollständig erklärt. |
|||
⚫ | Eine weitere Variante ohne zusätzlichen Speicher (in-place) besteht nur aus der Sammelphase, da hier jedes Fach nur 1 Bit breit ist. Das Zählen der beiden Bitzähler entfällt, da die beiden Bitwerte simultan von den beiden Grenzen des Arrays aufgefüllt werden und die beiden Schreibpositionen dabei aufeinander zulaufen. Da dieser Vorgang nicht stabil ist, muss bei der höchstwertigen Stelle (MSB) begonnen werden. |
||
{{Vorlage:Belege fehlen|Beanstandet im Juli 2019}} |
|||
⚫ | Eine weitere Variante ohne zusätzlichen Speicher (in-place) besteht nur aus der Sammelphase, da hier jedes Fach nur 1 Bit breit ist. Das Zählen der beiden Bitzähler entfällt, da die beiden Bitwerte simultan von den beiden Grenzen des Arrays aufgefüllt werden und die beiden Schreibpositionen dabei aufeinander zulaufen. Da dieser Vorgang nicht stabil ist, muss bei der höchstwertigen Stelle ([[#MSB|MSB]]) begonnen werden. |
||
== Laufzeit == |
== Laufzeit == |
Version vom 1. Juli 2019, 12:23 Uhr
Radixsort (von lateinisch radix ‚Wurzel‘, ‚Basis‘) oder auch Distributionsort (von englisch distribution ‚Verteilung‘), oder im Deutschen Fachverteilen, ist ein lineares Sortierverfahren, das auf Countingsort oder Bucketsort basiert. Das Sortierverfahren hat, unter der Voraussetzung, dass die maximale Länge der zu sortierenden Schlüssel von vornherein bekannt ist, eine lineare Laufzeit. Die Vorgehensweise eines Lochkartensortierers entspricht einem Radixsort.
Bei allen hier vorgestellten Varianten ist die erste Stelle des Schlüssels diejenige mit dem höchsten Rang. Man unterscheidet Verfahren, deren erster Schritt an dieser höchstwertigen Stelle (MSD) (engl. most significant digit) beginnt, dann können Teilstücke, die nach einem Verfahrensschritt weniger als zwei Elemente enthalten, im nachfolgenden Schritt übersprungen werden; und Verfahren, die an der niedrigstwertigen Stelle (LSD) (engl. least significant digit) beginnen und sich zur höchstwertigen Stelle vorarbeiten.
Ferner gibt es stabile out-of-place Varianten und in-place Varianten, die jedoch nicht stabil sind.
Voraussetzungen
Bei Radixsort bestehen die Schlüssel der zu sortierenden Daten aus Zeichen eines endlichen Alphabets. Zusätzlich muss eine totale Quasiordnung zwischen den Zeichen des Alphabets bestehen, meist ist es sogar eine Totalordnung. Damit ähneln die Schlüssel Zahlen in einem Stellenwertsystem mit der Mächtigkeit des Alphabets als Basis (oder Radix).
Eine zweite Voraussetzung ist, dass die Länge der Schlüssel durch eine von vornherein bekannte Konstante begrenzt ist, dann ist das Laufzeitverhalten linear in der Anzahl der Elemente.
Vorgehensweise (mittels Listen)
Radixsort besteht aus mehreren Schritten, und jeder Schritt aus zwei Phasen. Die Partitionierungsphase dient dazu, die Daten auf Fächer aufzuteilen, während in der Sammelphase die Daten aus diesen Fächern wieder aufgesammelt werden. Beide Phasen werden für jede Stelle des zu sortierenden Schlüssels einmal durchgeführt. Die Anzahl der Schritte ist gleich der (maximalen) Stellenanzahl.
- Partitionierungsphase
- In dieser Phase werden die Daten in die vorhandenen Fächer aufgeteilt, wobei für jedes Zeichen des zugrundeliegenden Alphabets ein Fach zur Verfügung steht. In welches Fach der gerade betrachtete Schlüssel gelegt wird, wird durch das an der gerade betrachteten Stelle stehende Zeichen bestimmt. So wird zum Beispiel die Zahl 352 in das Fach 3 gelegt, wenn gerade die dritte Stelle von hinten betrachtet wird (und wenn 10 die Basis (Radix) der Zahldarstellung ist).
- Sammelphase
- Nach der Aufteilung der Daten in Fächer in Phase 1 werden die Daten wieder eingesammelt und auf einen Stapel gelegt. Hierbei wird so vorgegangen, dass zuerst alle Daten aus dem Fach mit der niedrigsten Wertigkeit eingesammelt werden, wobei die Reihenfolge der darin befindlichen Elemente nicht verändert werden darf. Danach werden die Elemente des nächsthöheren Faches eingesammelt und an die schon aufgesammelten Elemente angefügt. Dies führt man fort, bis alle Fächer wieder geleert wurden.
Diese beiden Phasen werden nun für jede Stelle der Schlüssel wiederholt, wobei mit der letzten Stelle begonnen wird (LSD (engl. least significant digit) Radixsort). Bei jedem Schritt wird dieselbe Anzahl von Fächern benötigt. Und beim letzten Schritt wird die erste Stelle zum Aufteilen verwendet. Nach der letzten Sammelphase sind die Daten aufsteigend sortiert.
Alternativ können die Stellen des Schlüssels auch von der höchstwertigen her (MSD (engl. most significant digit) Radixsort) bearbeitet werden. Hierbei sind bei jedem Schritt zu jedem Fach Unterfächer zu bilden. Allerdings benötigen (Unter)fächer mit weniger als zwei Elementen keine weitere Unterteilung.
Vorgehensweise (via Countingsort)
Es ist auch ein anderer Ansatz möglich, der auch zwei Phasen benötigt. Diese Variante erspart sich die Verwaltung von variablen Listen und benötigt zwei Arrays.
- Zählphase
Zuerst werden die Füllstande der Fächer durch Zählen ermittelt. Das erste Array für stellenweises Countingsort ergibt ein Histogramm über das Alphabet. Danach wird das Histogramm in sein (linksseitiges) Integral umgewandelt (wobei das erste Element stets Null ist).
- Sammelphase
Nun wird mittels Histogrammintegral jedes Datenelement einmalig an seine finale Position verschoben (final für diese Runde). Dazu muss nach jedem Zugriff auf das Histogrammintegral der jeweilige Zähler inkrementiert werden, da er als Zeiger auf die Schreibposition dient. Das zweite Array dient dem temporären Speichern der Datenelemente und hat den gleichen Speicherbedarf wie das ursprüngliche Datenarray.
Vorgehensweise (in-place)
Verbessern: Die Funktionsweise ist unvollständig erklärt. Kommentar
Eine weitere Variante ohne zusätzlichen Speicher (in-place) besteht nur aus der Sammelphase, da hier jedes Fach nur 1 Bit breit ist. Das Zählen der beiden Bitzähler entfällt, da die beiden Bitwerte simultan von den beiden Grenzen des Arrays aufgefüllt werden und die beiden Schreibpositionen dabei aufeinander zulaufen. Da dieser Vorgang nicht stabil ist, muss bei der höchstwertigen Stelle (MSB) begonnen werden.
Laufzeit
Die Laufzeit des Algorithmus lässt sich durch (siehe Landau-Symbole) abschätzen, wobei die Länge eines Schlüssels und die Anzahl der zu sortierenden Elemente bezeichnet. Da für primitive Datentypen wie Ganzzahlen oder Gleitkommazahlen konstant ist, hat Radixsort für diese Fälle eine Laufzeit, die linear proportional mit der Anzahl der zu sortierenden Elemente zunimmt, womit es besser ist als andere, vergleichsbasierte Sortierverfahren wie beispielsweise Quicksort. Für variabel-lange Datentypen wie Listen oder Zeichenketten sind Quicksort oder Introsort jedoch u. U. die bessere Wahl. Jedoch ist der Aufwand für jeden Vergleich von Zeichenketten auch linear von ihrer (durchschnittlichen) Länge abhängig, da auch hier eine Zerlegung in die maximale Registerbreite (z. B. 64 Bit) stattfinden muss. Streng genommen ist der Aufwand für vergleichsbasierte Sortierverfahren auch proportional zu , wobei hier effektiv kleiner ist.
Besonderheit
Der Radixsort kann entweder stabil (d. h. duplikate Schlüssel treten nach der Sortierung in der gleichen Reihenfolge auf wie in der Ursprungsmenge) oder in-place (d. h. kein zusätzlicher Speicher nötig) realisiert werden.
Beispiel
Es sollen folgende Zahlen geordnet werden:
124, 523, 483, 128, 923, 584
Zunächst wird nach der letzten Stelle geordnet (LSD Radixsort).
Es beginnt die Partitionierungsphase:
|0| |1| |2| |3| |4| |5| |6| |7| |8| |9| | | | 523 124 128 483 584 923
Für die Stabilität des Verfahrens ist es wichtig, dass die angezeigte Reihenfolge der Elemente in den Fächern |3|
und |4|
eingehalten wird.
Es folgt die Sammelphase (Elemente von oben nach unten, von links nach rechts aufsammeln):
523, 483, 923, 124, 584, 128
Nun wird nach der nächsten Stelle der Zahlen geordnet (zweite Stelle von hinten nach vorne).
Erneute Partitionierungsphase:
|0| |1| |2| |3| |4| |5| |6| |7| |8| |9| | | 523 483 923 584 124 128
Für das Funktionieren des Verfahrens ist es wichtig, dass die angezeigte Reihenfolge der Elemente in den Fächern |2|
und |8|
eingehalten wird.
Entsprechendes gilt auch für die späteren Schritte.
Nun eine zweite Sammelphase:
523, 923, 124, 128, 483, 584
Und jetzt wird nach der ersten Stelle geordnet.
Die letzte Partitionierungsphase:
|0| |1| |2| |3| |4| |5| |6| |7| |8| |9| | | | | 124 483 523 923 128 584
Es folgt die letzte Sammelphase:
124, 128, 483, 523, 584, 923
Die Zahlen sind nun aufsteigend sortiert.
Implementierung
Iterativ
Java-Methode zum Sortieren von Integer-Arrays:
// Achtung: Integer ist in Java immer vorzeichenbehaftet (=signed).
// Der folgende Code sortiert jedoch so, als ob der Datentyp int kein Vorzeichen hätte (=unsigned)
// und funktioniert daher in Java nur bei positiven Zahlen.
public static void radixSort(int[] a) {
int n; // Fachnummer
int[] nPart = new int[2]; // Anzahl der Elemente in den beiden Faechern
int[][] part = new int[2][a.length]; // die beiden Faecher haben die Groesse des Arrays a
// Schleife ueber alle Bits der Schluessel (bei int: 32 Bit)
for (int i=0; i<32; i++) {
nPart[0] = 0;
nPart[1] = 0;
// Partitionierungsphase: teilt "a" auf die Faecher auf
for (int j=0; j<a.length; j++) {
n = (a[j]>>i)&1; // ermittelt die Fachnummer: 0 oder 1
part[n][nPart[n]++] = a[j]; // kopiert j-tes Element ins richtige Fach
}
// Sammelphase: kopiert die beiden Faecher wieder nach "a" zusammen
System.arraycopy(part[0], 0, a, 0, nPart[0]);
System.arraycopy(part[1], 0, a, nPart[0], nPart[1]);
}
}
Rekursiv
C++ Funktion zum Sortieren von Integerarrays und -Containern, die mindestens einen Forward-Iterator anbieten:
#include <algorithm>
#include <map>
#include <vector>
template <typename ForwardIterator>
void radixsort(const ForwardIterator first, const ForwardIterator last, int factor = 10)
{
// partitionieren
std::map<int, std::vector<int> > buckets;
for (ForwardIterator i = first; i != last; ++i) {
// die gewünschte Ziffer ermitteln und im Bucket mappen
if (factor == 10) buckets[*i%factor].push_back(*i);
else buckets[(*i/(factor/10)) %10].push_back(*i);
}
// sammeln
ForwardIterator copyfirst = first;
for (int i = 0; i < 10; ++i) {
for (std::vector<int>::const_iterator it = buckets[i].begin(); it != buckets[i].end(); )
// Sammeln und Änderungen auf den Iteratorbereich [first, last) anwenden
*copyfirst++ = *it++;
}
if (factor > *std::max_element(first, last)) {
// Höchstwertigste Ziffer bereits erreicht
return;
} else {
factor *= 10;
radixsort(first, last, factor);
}
}
Siehe auch
Literatur
- Donald Knuth. The Art of Computer Programming, Volume 3: Sorting and Searching, Third Edition. Addison-Wesley, 1997. ISBN 0-201-89685-0. Section 5.2.5: Sorting by Distribution, pp. 168–179.
- Robert Sedgewick: Algorithms in C++, third edition, 1998, p. 424–427.
- V. J. Duvanenko: In-Place Hybrid Binary-Radix Sort, Dr. Dobb’s Journal, 1 October 2009.