Radixsort ist ein sehr interessantes Sortierverfahren - basierend auf dem BehĂ€ltersortieren. Wir stellen beide Algorithmen vor und implementieren sie Schritt fĂŒr Schritt in Modula-2 und Omikron.BASIC.
Sie kennen doch sicher das Kartenspiel 32-heb-auf. Nicht? Kein Problem. Es gibt nur eine Regel: Werfen Sie ein komplettes Skat-Spiel auf den Boden. Alles weitere ergibt sich aus dem Namen - 32-heb-auf. Ăbrigens spielen die meisten dieses Spiel nur einmal selbst, aber umso öfter mit anderen.
Wir wollen zunĂ€chst Ihre Fingerfertigkeit testen. Böse Zungen behaupten, daĂ sich bei Computer-Freaks die Anwendung dieser GliedmaĂen auf die BetĂ€tigung von Tasten beschrĂ€nkt. Beweisen Sie das Gegenteil: Die Karten dĂŒrften nach Ihrer Sammelaktion unter dem Schreibtisch gut gemischt sein. Ihre erste Aufgabe ist, die Karten zu sortieren - rein manuell versteht sich.
Doch halt! Nicht so voreilig. Wir stellen Ihnen ein schnelles Sortierverfahren vor.
Klappe: Spielkartensortieren - die erste
Schaffen Sie vor sich soviel Platz, daĂ Sie acht Kartenstapel nebeneinander legen können - fĂŒr jeden Kartenwert einen. Der linke Stapel sei fĂŒr die Siebenen, der rechts daneben fĂŒr die Achten, der nĂ€chste fĂŒr die Neunen und so weiter. Ganz rechts kommen die Asse zu liegen.
Legen Sie Ihren gemischten Kartenstapel mit dem Blatt nach oben vor sich hin. Nehmen Sie die oberste Karte und verteilen Sie diese bezĂŒglich Ihres Werts auf einen der acht Stapel. Dies wiederholen Sie solange, bis der gemischte Stapel leer ist (32mal). Jeder Stapel enthĂ€lt anschlieĂend vier Karten. Legen Sie alle Stapel von rechts nach links aufeinander - zuerst die Asse auf die Könige, diese acht Karten auf die Damen und so weiter.
Im zweiten Durchgang verteilen Sie die Karten auf vier Stapel - fĂŒr jede Farbe einen. Die Reihenfolge ist von links nach rechts: Kreuz, Pik, Herz, Karo. Legen Sie immer nur die oberste Karte Ihres vorsortierten Kartenstapels auf einen der Farbenhaufen. AnschlieĂend enthalten alle vier Stapel acht Karten. Legen Sie die Stapel genau umgekehrt zum ersten Durchgang aufeinander - von links nach rechts: Der Kreuz-Stapel kommt auf den fĂŒr die Pik-Karten, diese zusammen auf den Herz- und zum SchluĂ alles auf den Karo-Stapel.
Die Karten sind jetzt in der Reihenfolge Kreuz-7 - Kreuz-As, Pik-7 - Pik-As, Herz-7 - Herz-As, Karo-7 - Karo-As sortiert.
Suchen Sie sich nun bitte einen nettenâ Zeitgenossen und spielen Sie mit ihm 32-heb-auf. Falls gerade keiner zur Hand ist, dĂŒrfen Sie die Karten auch anders mischen.
Klappe: Spielkartensortieren - die zweite
Wir modifizieren das Sortierverfahren ein wenig: Verteilen Sie die Karten zuerst in die vier Farbenstapel und legen Sie die Haufen von rechts nach links ĂŒbereinander. AnschlieĂend verteilen Sie sie auf die acht Wert-Stapel und sammeln von links nach rechts. Auch jetzt sind die Karten sortiert, aber in einer anderen Reihenfolge: Kreuz-7 - Karo-7, Kreuz-8 - Karo-8, âŠ, Kreuz-As - Karo-As.
Unten zeigen wir, warum das Kartensortieren funktioniert. Sie werden sehen, daà es eine praktische Anwendung des BehÀltersortierens und der Radix-Sortierung ist.
BehÀltersortieren
Im ersten Kursteil haben wir beschrieben, daĂ ein Datentyp die Menge von Werten bezeichnet, die ein Objekt annehmen kann.
TYPE Letter=["A".."Z"]
TYPE Digit=["0".."9"]
Letter und Digit sind selbstdefinierte Modula-2-Datentypen. Letter ist ein Name fĂŒr die Menge aller GroĂbuchstaben (ohne Umlaute). Digit umfaĂt alle Dezimalziffern. Die KardinalitĂ€t eines Datentyps ist die Anzahl der zu diesem Typ gehörenden Werte. Man kennzeichnet die KardinalitĂ€t durch Voranstellen von â#' oder durch Umklammern mit zwei senkrechten Strichen (Betragsstriche). Im folgenden sind KardinalitĂ€ten fĂŒr einige Datentypen aufgelistet.
#Letter=26
#Digit=10
#CHAR=256
#INTEGER=65536
#BOOLEAN=2
Wir beschĂ€ftigen uns im folgenden mit der Sortierung von Objekten. Als Objekt bezeichnen wir den Wert eines beliebigen Datentyps. Beispiele sind Zahlen, Strings, Records oder Felder. Die Objekte sortieren wir nach ihren SchlĂŒsselwerten. Vereinfachend nennt man den SchlĂŒsselwert auch SchlĂŒssel. Das SchlĂŒsselfeld ist die Komponente des Objekts, die den SchlĂŒsselwert enthĂ€lt.
(**********************************************
* Algorithmus zum BehÀltersortieren bei *
* verschiedenen SchlĂŒsselwerten *
**********************************************)
CONST MaxN=<Anzahl maximal zu sortierender Records>
MinKey= <kleinster mögl. SchlĂŒsselwert>
MaxKey= <gröĂter mögl. SchlĂŒsselwert>
TYPE KeyTyp = [MinKey..MaxKey]; (* Datentyp fĂŒr die SchlĂŒssel *)
ObjTyp=RECORD (* Datentyp fĂŒr die zu sortierenden Objekte *)
Key : KeyTyp (* SchlĂŒsselfeld *)
<weitere Komponenten>
END;
...
VAR A : ARRAY [1..MaxN] OF ObjTyp; (* zu sortierendes Feld *)
B : ARRAY KeyTyp OF ObjTyp; (* BehÀlter *)
N : CARDINAL; (* Anzahl zu sortierender Records *)
j : KeyTyp; (* Laufvariable *)
i : CARDINAL; (* Laufvariable *)
...
BEGIN
(* BehÀlter löschen *)
FOR j:=MinKey TO MaxKey DO
<B[j] als leer markieren>
END;
(* Sortieren *)
FOR i:=1 to N DO
B[A[i].Key]:=A[i]
END;
(* sortiert zurĂŒckschreiben *)
i : =0 ;
FOR j:=MinKey TO MaxKey DO
IF <B[j] ist nicht leer> THEN
A[i]:=B[j];
i : =i + 1
END;
END;
END;
Bild 10
Bei Datentypen mit kleiner KardinalitĂ€t bietet sich zum Sortieren das BehĂ€ltersortieren (Fachsortieren, engl, binsort) an. FĂŒr jeden möglichen SchlĂŒsselwert stellen wir einen BehĂ€lter (Fach, engl. bin) bereit, der die Objekte mit diesem SchlĂŒsselwert aufnimmt.
Dieses Prinzip verwendet man abgewandelt in der Industrie. Kartoffeln sortiert man bezĂŒglich ihrer GröĂe, indem man sie ĂŒber Siebe mit verschieden groĂen Löchern leitet. Unter jedem Sieb steht ein BehĂ€lter, der die Kartoffeln dieser GröĂe aufnimmt.
Im ersten Schritt unserer Kartensortierung legen wir fĂŒr jeden Kartenwert einen Stapel an. Diese Stapel sind die BehĂ€lter.
Auf den SchlĂŒsselwerten muĂ eine (totale) Ordnung definiert >ein. Die Ordnung ergibt sich meist aus dem Kontext. Bei Zahlen ist klar, daĂ fĂŒnf kleiner als sieben ist. Ebenso sortieren wir Anna vor Rudolf. Datentypen fĂŒr SchlĂŒsselwerte mit beschrĂ€nkter KardinalitĂ€t haben einen kleinsten und einen gröĂten SchlĂŒsselwert. Diese bezeichnen wir meist mit MinKey beziehungsweise MaxKey. Beide sind eindeutig.
Verschiedene SchlĂŒsselwerte
Betrachten wir zunĂ€chst den einfachsten Fall: Alle zu sortierenden Datenobjekte haben verschiedene SchlĂŒsselwerte. Somit speichert ein BehĂ€lter maximal ein Objekt.
Die BehĂ€lter realisieren wir in einem Feld. Dieses stellt fĂŒr jeden möglichen SchlĂŒsselwert genau einen BehĂ€lter bereit.
Betrachten wir beispielsweise die folgende Datentypdefinition und Variablendeklaration.
CONST MinKey = 1;
MaxKey = 5;
TYPE KeyTyp = [MinKey..MaxKey];
ObjTyp = RECORD
Key : KeyTyp;
Text: String;
END;
VAR A : ARRAY [0..2] OF ObjTyp;
ObjTyp ist der Datentyp der zu sortierenden Objekte. ObjTyp enthĂ€lt ein SchlĂŒsselfeld namens Key (SchlĂŒssel) vom Datentyp KeyTyp. Der SchlĂŒsselwert in Key ist eine ganze Zahl zwischen eins und fĂŒnf. Die Informationskomponente Text speichert eine Zeichenkette. Das Record-Feld A enthĂ€lt drei Records des Datentyps ObjTyp.
Wir wollen ein entsprechendes Feld A() in Omikron.BASIC sortieren. Die Felddimensionierungen und exemplarischen Variablenvorbelegungen lauten:
DIM A_Key%(2), A_Text$(2)
A_Key%(0)=5: A_Text$(0)="Maus"
A_Key%(1)=2: A_Text$(1)="Hund"
A_Key%(2)=4: A_Text$(2)="Katze"
Wir sortieren nicht die Zeichenketten, sondern die Zahlen (SchlĂŒsselwerte). Zugelassen haben wir die SchlĂŒsselwerte eins bis fĂŒnf. Wir benötigen also fĂŒnf verschiedene BehĂ€lter. Allgemein gilt:
Die KardinalitĂ€t des SchlĂŒsselwertdatentyps ist die Anzahl der benötigten BehĂ€lter.
Die BehÀlter speichern wir in einem Record-Feld, das wir B() nennen. Der Datentyp von B() muà den Datentyp von A() enthalten:
DIM B_Key%(5), B_Text$(5)
Der Einfachheit halber spendieren wir einen zusÀtzlichen BehÀlter mit dem Index Null. Wir sortieren in einer einzigen Schleife:
'BehĂ€ltersortieren'verschiedene SchlĂŒsselwerte
FOR I%=0 TO 2
B_Key%(A_Key%(I%))=A_Key%(I%)
B_Text$(A_Key%(I%))=A_Text$(I%)
NEXT I%
enthĂ€lt anschlieĂend folgende Werte
B_Key%(0)=0 B_Text$(0)=""
B_Key%(1)=0 B_Text$(1)=""
B_Key%(2)=2 B_Text$(2)="Hund"
B_Key%(3)=0 B_Text$(3)=""
B_Key%(4)=4 B_Text$(4)="Katze"
B_Key%(5)=5 B_Text$(5)="Maus"
Der Wert Null kennzeichnet einen leeren BehĂ€lter. Von oben nach unten betrachtet sehen Sie in B() die sortierte Reihenfolge der SchlĂŒssel: 2, 4, 5. Die sortierten Records schreiben wir nun in das Feld A() zurĂŒck:
' BehÀlter auflösen
I% = 0
FOR J%=1 TO 5
IF B_Key%(J%)<>0 THEN
A_Key%(I%)=B_Key%(J%)
A_Text$(I%)=B_Text$(J%)
I%=I%+1
ENDIF
NEXT J%
AnschlieĂend enthĂ€lt das Feld A() folgende Werte:
A_Key%(0)=2: A_Text$(0)="Hund"
A_Key%(1)=4: A_Text$(1)="Katze"
A_Key%(2)=5: A_Text$(2)="Maus"
Bild 10 zeigt den Algorithmus in Modula-2-Syntax.
KeyTyp kann theoretisch anstelle eines Unterbereichstyps auch ein AufzÀhlungstyp sein.
Ein Problem ist die Kennzeichnung eines leeren BehÀlters. Zwei Wege zur Realisierung bieten sich an: Wir ergÀnzen KeyTyp um einen Wert, der einen leeren BehÀlter markiert. Diesen Weg sind wir in der Omikron.BASIC-Version mit dem Wert Null gegangen. Als Alternative erweitern wir den Datentyp von B() um ein Flag. Dieses signalisiert, ob der BehÀlter belegt oder leer ist. Wir gehen auf dieses Problem nicht weiter ein, da es bei unseren spÀteren Algorithmen wegfÀllt.
Beliebige SchlĂŒsselwerte
Vereinfachend sind wir davon ausgegangen, daĂ jeder SchlĂŒssel wert maximal einmal vorkommt. Diese Voraussetzung ist nur selten gegeben. Wir umgehen dieses Problem, indem jeder BehĂ€lter Objekte mit gleichen SchlĂŒsseln in einer Warteschlange (engl, queue) speichert. Also: pro BehĂ€lter eine Schlange.
Im Hinblick auf Radixsort sortieren wir im folgenden Schlangen anstatt Felder. Als grobe Struktur ergibt sich somit:
gegeben: unsortiertes Feld A
gesucht: sortiertes Feld A
- unsortiertes Feld A in Schlange S ĂŒbertragen
- Schlange S sortieren
- sortierte Schlange S in das Feld A zurĂŒckschreiben
Ein groĂer Vorteil dieses Aufbaus sticht sofort ins Auge: Falls die zu sortierenden Daten bereits als Schlange organisiert sind, entfallen die Schritte 1. und 2. Diese Situation liegt in Modula-2-Programmen meist vor.
Die Ăbertragung von Feldern in Schlangen und umgekehrt beschreiben wir im nĂ€chsten Kursteil. Wir befassen uns vorerst mit Schritt 2 - der Sortierung einer Schlange S in BehĂ€lter B().
Bild 12
Den groben Aufbau des BehĂ€ltersortierens haben wir in unseren EinfĂŒhrungsbeispielen vorgestellt. Bild 11 zeigt den BehĂ€ltersortier-Algorithmus fĂŒr beliebige SchlĂŒssel. Bild 12 verdeutlicht die Arbeitsweise bei der Sortierung von Namen. Es wird nur nach den Anfangsbuchstaben geordnet. Diese sind explizit als SchlĂŒsselwerte angegeben.
(**********************************************
* Algorithmus zum BehÀltersortieren *
**********************************************)
CONST
MinKey = <kleinster möglicher SchlĂŒsselwert>
MaxKey = <gröĂter möglicher SchlĂŒsselwert>
TYPE
KeyTyp = <Datentyp, der die möglichen
SchlĂŒsselwerte festlegt. Auf KeyTyp
muĂ eine Ordnung definiert sein.
MinKey und MaxKey sind der
kleinste bzw. gröĂte mögliche Wert>
ObjTyp=RECORD (* Datentyp fĂŒr die zu sortierenden Objekte *)
Key : KeyTyp; (* SchlĂŒsselfeld *)
Info : InfoTyp; (* beliebig viele Infokomponenten *)
END;
STyp = <Datentyp fĂŒr die Realisierung einer
Schlange, deren Elemente vom Datentyp
ObjTyp sind.>
...
VAR
S : STyp; (* zu sortierende Schlange *)
B = ARRAY KeyTyp OF STyp;
(* BehÀlterfelder mit einem Element pro
möglichem SchlĂŒsselwert. Jeder
BehÀlter speichert eine Schlange. *)
i : KeyTyp; (* Laufvariable *)
...
BEGIN
(* Alle BehÀlter löschen *)
FOR i:=MinKey TO MaxKey DO
Lösche B(i)
END;
(* Schlange S in die BehÀlter B() sortieren *)
WHILE Schlange S nicht leer DO
Entferne Kopf von S und hÀnge ihn an die
Schlange des BehÀlters B(Kopf(S).Key)
END;
(* BehÀlter B() aufsammeln und sortierte Schlange S erzeugen. *)
Erzeuge leere Schlange S
FOR i:=MinKey TO MaxKey DO
Falls Schlange in B(i) nicht leer, hÀnge
sie an die Schlange S
END;
(* Die Schlange S ist nun sortiert *)
END;
Bild 11
BehÀltersortieren in Modula-2
Bild 13 zeigt geeignete Datentyp-Definitionen fĂŒr die Implementierung des BehĂ€ltersortierens in Modula-2.
#(* Allgemeine Datentypdefinitionen fĂŒr das BehĂ€ltersortieren *)
CONST MaxN=<Anzahl max. zu sortierender Records>
MinKey= <kleinster mögl.SchlĂŒsselwert>
MaxKey= <gröĂter mögl. SchlĂŒsselwert>
TYPE KeyTyp = [MinKey..MaxKey]; (* Wertebereich der SchlĂŒsselwerte *)
ObjTyp =RECORD (* Datentyp fĂŒr die zu sortierenden Objekte *)
Key KeyTyp;
<Infokomponenten>
END;
ObjLPtr= POINTER TO ObjLTyp;
ObjLTyp= RECORD (* Listenelement *)
Obj : ObjTyp;
Next : ObjLPtr
END;
STyp= RECORD (* Schlangen- = Listentyp *)
Anfang : ObjLPtr;
Ende : ObjLPtr;
END;
Bild 13
Die Definitionen sehen komplizierter aus, als sie sind. Bild 14 stellt die Struktur grafisch dar. Die zu sortierenden Objekte sind vom Datentyp ObjTyp. Dieser ist gegliedert in das SchlĂŒsselfeld Key und Informationskomponenten.
Ein BehĂ€lter ist eine Schlange. Schlangen realisieren wir als lineare Listen. Der Datentyp hierzu heiĂt STyp. Er speichert Zeiger auf den Anfang und das Ende der Schlange.
Die Listenelemente sind vom Typ Obj-LTyp. Ein Listenelement setzt sich aus dem zu sortierenden Objekt und einem Zeiger auf das nÀchste Listenelement zusammen. Die zugehörigen Variablendeklarationen lauten:
VAR S : STyp; (* zu sortierende Schlange *)
B : ARRAY KeyTyp OF STyp;
(* Feld mit BehÀltern *)
p : ObjLPtr; (* Hilfszeiger *)
i : KeyTyp; (* Laufvariable *)
Die BehÀlter löschen wir, indem wir jeweils den beiden Zeiger NIL zuweisen:
(* BehÀlter löschen *)
FOR i:=MinKey TO MaxKey DO
B[i].Anfang:=NIL;
B[i].Ende:=NIL
END;
Das Verteilen der Schlange auf die BehĂ€lter ist tĂŒckisch. Wir entfernen jeweils den Schlangenkopf und hĂ€ngen ihn an die Schlange des BehĂ€lters fĂŒr den zugehörigen SchlĂŒsselwert:
(* Schlange in BehÀlter sortieren *)
(* fehlerhaft *)
WHILE S.Anfang<>NIL DO
LLAnhaengen(B[S.Anfang^.Obj.Key],S.Anfang);
S.Anfang:=S.Anfang^.Next;
END;
Die Prozedur LLAnhaengen(VARL:STyp;P:ObjLPtr) aus Listing 4 hĂ€ngt das Objekt, auf das P zeigt, an das Ende der Liste L. Diese Operation entspricht dem EinfĂŒgen in eine Schlange (siehe zweiter Kursteil).
Finden Sie den Fehler in obigem Algorithmus? LLAnhaengen() hÀngt den Schlangenkopf an die Schlange eines BehÀlters. Dabei wird dem Next-Zeiger des Schlangenkopfs NIL zugewiesen, denn er markiert das BehÀlterschlangenende. Die nachfolgende Operation S.Anfang: = SAnfang^.Next weist S.Anfang somit den Wert NIL zu. Damit können wir auf den Rest der Schlange nicht mehr zugreifen. Die Schleife terminiert, nachdem das erste Element verteilt ist.
Dieser Fehler ist typisch fĂŒr Programme mit dynamischer Speicherverwaltung. Durch Unachtsamkeit verbiegt man wichtige Zeiger, so daĂ Daten zu Speicherleichen werden.
Abhilfe schafft ein Hilfszeiger p. p speichert das zweite Element von S, denn dieses ist im nÀchsten Schleifendurchlauf der Schlangenkopf:
Bild 14
Bild 15
(* Schlange in BehÀlter sortieren *)
WHILE S.Anfang<>NIL DO
p:=S.Anfang^.Next; (* Zeiger auf nÀchstes Element retten *)
LLAnhaengen(B[S.Anfang^.Obj.Key],S.Anfang);
S.Anfang:=p;
END;
Das Aufsammeln der BehÀlter ist einfach:
(* BehÀlter auflösen und sortierte Liste generieren *)
S.Anfang:=NIL;
S.Ende:=NIL;
FOR i:=MinKey TO MaxKey DO
IF B[i].Anfang<>NIL THEN
LLVerbinden(S,B[i])
END
END;
Die Prozedur LLVerbinden(VAR L1,L2:STyp) hĂ€ngt die Liste L2 an die Liste L1. Sie berĂŒcksichtigt die SonderfĂ€lle, daĂ die Listen leer sind.
Listing 4 enthĂ€lt die komplette Modula-2-Implementierung des BehĂ€ltersortierens mit einer Testumgebung. Das Programm sortiert Strings nach einem SchlĂŒssel vom Typ CHAR. Bild 15 ist eine Hardcopy der Ausgabe. Oben steht die unsortierte, unten die sortierte Liste.
BehÀltersortieren in Omikron.BASIC
Wenden wir uns der Implementierung des BehĂ€ltersortierens in Omikron.BASIC zu. In den folgenden Beispielen sortieren wir Worte (Strings) bezĂŒglich des Anfangsbuchstabens. Das SchlĂŒsselfeld ist das erste Element (Zeichen) des Strings. Der SchlĂŒsselwert ist der ASCII-Wert des Anfangsbuchstabens und nicht das Zeichen selbst.
Zur Realisation bietet sich die Datenstruktur Wortliste W1 an, die wir im zweiten Kursteil beschrieben haben:
DIM Wl_Wort$(Max_N%),
Wl_Next%(Max_N%)
Wir gehen davon aus, daĂ die Strings als Schlange organisiert sind. Diese Schlange S ist charakterisiert durch die beiden Cursor S_Anfang% und S_Ende%.
Min_Ascii% sei eine Konstante, die das Zeichen mit dem kleinsten ASCII-Wert festlegt, der sortiert wird. Entsprechend definiert Max_Ascii% den gröĂtmöglichen SchlĂŒsselwert. Die BehĂ€lter dimensionieren wir wie folgt:
DIM B_Anfang%(Max_Ascii%), B_Ende%(Max_Ascii%)
In Omikron.BASIC beginnen Felder mit dem Index Null. Die BehĂ€lter mit den Indizes von Null bis Min_Ascii%-1 sind somit ĂŒberflĂŒssig. Wir nehmen dies in Kauf, damit die Adressierung der BehĂ€lter einfach ist. Als erstes löschen wir die BehĂ€lter:
'BehÀlter löschen
FOR I%=Min_Ascii% TO Max_Ascii%
B_Anfang%(I%)=0
B_Ende%(I%)=0
NEXT I%
Die Sortierung ist aufwendiger als in Modula-2. Wir benutzen zwei Hilfsvariablen, um die Adressierung der BehĂ€lter ĂŒbersichtlich zu gestalten: Wort$ speichert den String, der in einen BehĂ€lter verteilt wird. Ch% ist der ASCII-Wert des Anfangsbuchstabens von Wort$. Im Hinblick auf die allgemeine String-Sortierung mit Radixsort bestimmen wir Ch% mit der Funktion MID$() und nicht mit LEFT$().
'S in BehÀlter sortieren
WHILE S_Anfang%<>0
P%=Wl_Next%(S_Anfang%) ' Cursor
'auf das zweite Listenelement
'retten
Wort$=Wl_Wort$(S_Anfang%)
Ch%=ASC(MID$(Wort$,1,1))
Ll_Anhaengen(B_Anfang%(Ch%),B_Ende%(Ch%),S_Anfang%)
S_Anfang%=P%
WEND
Die Prozedur Ll_Anhaengen() kennen wir bereits aus Listing 2 des zweiten Kursteils.
Zum Schluà lösen wir die BehÀlter auf und generieren die nach Anfangsbuchstaben sortierte Schlange:
'BehÀlter aufsammeln
FOR I%=Min_Ascii% TO Max_Ascii%
IF B_Anfang%(I%)<>0 THEN
Ll_Verbinden(S_Anfang%,S_Ende%,B_Anfang%(I%),B_Ende%(I%))
ENDIF
NEXT I%
Die Prozedur L1_Verhinden(R L1_Anfang%,R L1_Ende%, L2_Anfang%, L2_Ende%) hÀngt die Liste L2 an die Liste L1. Den Prozedurtext zeigt Listing 5.
Radixsort
Beim BehĂ€ltersortieren ist die KardinalitĂ€t des SchlĂŒsselwert-Datentyps die Anzahl der benötigten BehĂ€lter. Um Buchstaben mit unserem obigen Datentyp Letter zu sortieren, benötigen wir #Letter=26 BehĂ€lter. Entsprechend sortieren wir die Dezimalziffern von "0" bis "9" in #Digit=10 BehĂ€lter.
Das BehĂ€ltersortieren eignet sich nur fĂŒr Datentypen mit kleiner KardinalitĂ€t. Angenommen, Sie wollen Integer-Zahlen (16 Bit) in BehĂ€lter sortieren. Dazu brauchen Sie #INTEGER=65536 BehĂ€lter. Jeder BehĂ€lter speichert mindestens den SchlĂŒssel wert. Wir dimensionieren das BehĂ€lterfeld mit DIM B_Key%(65535) fĂŒr 65536 Integerzahlen (2 Byte). Das sind 128 KByte.
Bei 32-Bit-Zahlen ist diese Sortiermethode aus SpeicherplatzgrĂŒnden nicht realisierbar. Ebenso bei Zeichenketten. Angenommen Sie sortieren Strings mit maximal 3 Zeichen LĂ€nge. Es seien 128 verschiedene Zeichen zugelassen (7-Bit-Code). Dann brauchen Sie 128^3=2097152 BehĂ€lter.
Eine Erweiterung des BehĂ€ltersortierens ist das Radixsort. Wir haben den Grundgedanken des Radixsort im EinfĂŒhrungsbeispiel mit den Spielkarten kennengelernt.
(**********************************************
* Radixsort-Algorithmus *
**********************************************)
CONST
K = <Anzahl der SchlĂŒsselfelder>
MinKey1 = <kleinste mögliche SchlĂŒsselwerte
... fĂŒr die K SchlĂŒssel-Datentypen>
MinKeyK
MaxKey1 = <gröĂte mögliche SchlĂŒsselwerte
... fĂŒr die K SchlĂŒssel-Datentypen>
MaxKeyK
TYPE
KeyTyp1
...
KeyTypK = <Datentypen, die die möglichen SchlĂŒsselwerte festlegen>
ObjTyp=RECORD (* Datentyp fĂŒr die zu sortierenden Objekte *)
Key1 : KeyTyp1;
...
KeyK : KeyTypK;
Info : InfoTyp;
END
STyp = <Datentyp fĂŒr die Realisierung einer
Schlange deren Elemente vom Datentyp ObjTyp sind.>
VAR
S : STyp (* zu sortierende Schlange *)
B1 = ARRAY KeyTyp1 OF STyp
...
BK = ARRAY KeyTypK OF STyp
(* BehĂ€lterfelder mit einem Element pro möglichem SchlĂŒsselwert.
Jeder BehÀlter speichert eine Schlange. *)
i : <Laufvariable zur Selektion der SchlĂŒsselfelder>
j : <Laufvariable zum Zugriff auf die BehÀlter>
FOR i:=K DOWNTO 1 DO
(* BehĂ€ltersortieren bzgl. des SchlĂŒsselfelds Keyi *)
(* alle BehÀlter löschen *)
FOR j:=MinKeyi TO MaxKeyi DO
Lösche Bi(j)
END
(* Schlange S in die BehÀlter Bi() verteilen *)
WHILE Schlange S nicht leer DO
Entferne Kopf von S und hÀnge ihn an die
Schlange des zugehörigen BehÀlters
Bi(Kopf(S).Keyi)
END
(* BehÀlter Bi() aufsammeln und sortierte Schlange S erzeugen. *)
Erzeuge leere Schlange S
FOR j:=MinKeyi TO MaxKeyi DO
HĂ€nge Schlange Bi(j) an die Schlange S
END
(* Die Schlange S ist nun sortiert bezĂŒglich
der SchlĂŒsselfelder Keyi bis KeyK *)
END (* FOR i *)
(* Die Schlange S ist nun sortiert *)
Bild 16
Warum funktioniert das Spielkartensortierverfahren ?
Jede Spielkarte ist eindeutig durch Farbe (Kreuz, Pik, Herz oder Karo) und Wert (7-10, Bauer, Dame, König, As) charakterisiert. Im ersten Verfahren sortieren wir zuerst nach dem Wert in acht Stapel (BehĂ€lter). Die Karten in einem Stapel sind nur bezĂŒglich ihrer Farbe gemischt. Nachdem die Stapel aufeinandergelegt sind, liegen die vier Asse oben, die vier Siebenen unten.
Im zweiten Durchgang können zwei FĂ€lle eintreten: Haben zwei Karten die gleiche Farbe, so kommen sie auf den selben Stapel. Der erste Durchlauf sorgt dafĂŒr, daĂ Karten mit hohem Wert unter Karten mit niedrigem Wert liegen. Also sortieren Sie richtig. Im zweiten Fall haben zwei Karten verschiedene Farben.
Sie kommen somit auf verschiedene Stapel. Das Auflesen der Stapel besorgt die Sortierung.
Wir mĂŒssen noch zeigen, daĂ das Auflesen der Stapel korrekt ist. Es ist zunĂ€chst uneinsichtig, warum wir die Stapel einmal von links und einmal von rechts aufeinander legen. Dies ist ein Tribut an die Nutzbarkeit der Methode.
Das BehÀltersortieren basiert auf FIFO- (Schlange) und nicht auf LIFO-Stapeln (Keller). Es gibt drei Alternativen, um dies bei Karten zu realisieren:
Wir schieben eine neue Karte unter den schon vorhandenen Stapel. Dies ist technisch sehr aufwendig. AnschlieĂend sammeln wir die FIFO-Stapel immer von links nach rechts.
Bei der zweiten Alternative können wir die Karten schneller verteilen: Wir legen die Karten mit dem Blatt nach unten auf die Stapel und sammeln immer von rechts nach links. Dieses Vorgehen entspricht dem ersten, wenn Sie es von unten betrachten. Der Nachteil: Sie sehen den Wert der einzelnen Stapel nicht mehr -Spielkarten sind auf dem RĂŒcken einheitlich.
Die dritte Alternative haben wir oben vorgestellt: Die Karten werden mit dem Blatt nach oben auf die LIFO-Stapel (Keller) gelegt. Die Sammelrichtung wechselt. Wenn Sie einen Keller von oben abbauen und nebenan wiederaufbauen, vertauschen Sie die Reihenfolge der Elemente. Sie ĂŒberfĂŒhren den LIFO- in einen FIFO-Stapel (Schlange). Dies macht sich unser Verfahren zunutze.
Diese drei Alternativen fĂŒhren zum gleichen Ergebnis. Wir haben somit gezeigt, daĂ das Sortierverfahren korrekt ist.
Das Spielkartensortieren demonstriert den Grundgedanken des Radixsort: Man teilt ein SchlĂŒsselfeld mit groĂer KardinalitĂ€t in mehrere SchlĂŒsselfelder mit kleiner KardinalitĂ€t. Auf jedes dieser TeilschlĂŒsselfelder wendet man das BehĂ€ltersortieren an.
Die TeilschlĂŒsselfelder interpretiert man bezĂŒglich verschiedener Basen (Basis = engl. radix). Daher kommt der Name Radixsort.
Betrachten wir die Datenstruktur, die eine Spielkarte beschreibt.
TYPE
FarbTyp=(Kreuz, Pik, Herz, Karo);
WertTyp=(Sieben, Acht, Neun, Zehn, Bauer, Dame, König, Ass)
Karte=RECORD
Farbe : FarbTyp;
Wert : WertTyp;
END;
Die Datentypen FarbTyp und WertTyp nennt man in Modula-2 AufzÀhlungstypen, da alle möglichen Werte inklusive Ordnung bei der Definition aufgezÀhlt sind.
Die KardinalitĂ€t von Karte ist 32, denn es gibt acht Werte mit jeweils vier Farben. Wir benötigen fĂŒr das BehĂ€ltersortieren bezĂŒglich Karte somit 32 BehĂ€lter. Es liegt nahe, zwei getrennte SortierlĂ€ufe zu starten. Einen bezĂŒglich des TeilschlĂŒsselfelds Farbe und einen bezĂŒglich des TeilschlĂŒsselfelds Wert. Es genĂŒgen einmal #Farbe=4 und einmal #Wert=8 BehĂ€lter.
Im folgenden bezeichnen wir die TeilschlĂŒsselfelder als SchlĂŒsselfelder. Wir benötigen eine PrioritĂ€tsregelung der SchlĂŒsselfelder, um die Reihenfolge der SortierlĂ€ufe festzulegen. Je gröĂer die PrioritĂ€t eines SchlĂŒsselfelds ist, um so gröĂeren EinfluĂ hat es auf die Sortierung. PrimĂ€r ordnen wir bezĂŒglich des SchlĂŒsselfelds mit der höchsten PrioritĂ€t.
Nach Anwendung des ersten Verfahrens sind die Karten primÀr nach Farben sortiert: Kreuz-7 - Kreuz-As, Pik-7 - Pik-As, Herz-7 - Herz-As, Karo-7 - Karo-As. Das zweite Verfahren ordnet primÀr nach Werten: Kreuz-7 - Karo-7, Kreuz-8 - Karo-8,..., Kreuz-As - Karo-As.
In welcher Reihenfolge fĂŒhren wir die einzelnen DurchlĂ€ufe aus? Allgemein gilt: Je niedriger die PrioritĂ€t eines SchlĂŒsselfelds, desto frĂŒher sortieren wir nach diesem. Wir beginnen mit dem am wenigsten signifikanten SchlĂŒsselfeld. Zum SchluĂ sortieren wir bezĂŒglich des SchlĂŒsselfeldes mit der höchsten PrioritĂ€t.
Mit dem gleichen Verfahren sortieren Sie auch mehrere Kartenspiele: Betrachten Sie die Kartenspielart als dritten SchlĂŒssel. Verteilen Sie zuerst nach Wert und Farbe. Im dritten Durchlauf ordnen Sie die verschiedenen Kartenspiele.
Allgemein sortiert Radixsort eine Datenstruktur mit dem Aufbau
TYPE ObjTyp=RECORD
Key1 : KeyTyp1;
...
KeyK : KeyTypK;
Info : InfoTyp;
END
Key1 bis KeyK sind K SchlĂŒsselfelder. Key1 habe die höchste und KeyK die niedrigste PrioritĂ€t. Info steht stellvertretend fĂŒr weitere Komponenten.
FĂŒr jedes SchlĂŒsselfeld benötigen wir eigene BehĂ€lter Bi(). Der Index i besagt, daĂ die BehĂ€lter zur Sortierung bezĂŒglich des SchlĂŒsselfeldes KeyTypi dienen. Als grobe Strukur des Radixsort-Algorithmus' ergibt sich somit
FOR i:=K DOWNTO 1 DO
BehÀltersortieren bzgl.
SchlĂŒsselfelds Keyi
in die BehÀlter Bi()
END
Die FOR-Schleife beginnt mit dem SchlĂŒsselfeld, das die niedrigste PrioritĂ€t hat. Zum SchluĂ sortiert sie bezĂŒglich Keyi - dem SchlĂŒsselfeld mit der höchsten PrioritĂ€t. Bild 16 zeigt den kompletten Algorithmus zur Radixsortierung.
Versuchen Sie einmal, Radixsort auf die Sortierung von Strings anzuwenden. Wir stellen im nÀchsten Kursteil die Lösung und einige Optimierungen in Omikron.BASIC und Modula-2 vor.
Literatur:
Aho, Hopcorft, Ullman, Data Structures and Algorithms, Addison Wesley Donald E. Knuth, The Art of Computer Programming, Vol. 3 Sorting and Searching, Addison Wesley, S. 170ff
Kurt Mehlhorn, Datenstrukturen und effiziente Algorithmen, Band 1 Sortieren und Suchen, B.G. Teuhner Stuttgart
(**********************************************
* Listing 4 *
* BehÀltersortieren mit linearen Listen *
* *
* Sven KrĂŒppel 1.1.1991, (c) MAXON Computer *
* geschrieben mit dem ETH-Modula 2-System *
**********************************************)
MODULE Listing4;
FROM InOut IMPORT Write,WriteString,WriteLn;
FROM Heap IMPORT Allocate, Deallocate;
CONST MaxN=20; (* Anzahl max. zu sortierender Records *)
MinKey="A"; (* kleinster SchlĂŒsselwert *)
MaxKey="Z"; (* gröĂter SchlĂŒsselwert *)
MaxLaenge=15; (* max. StringlÀnge *)
TYPE String = ARRAY [1..MaxLaenge] OF CHAR;
KeyTyp = [MinKey..MaxKey]; (* Wertebereich der SchlĂŒsselwerte *)
ObjTyp = RECORD (* Record fĂŒr Daten *)
Key : KeyTyp; (* SchlĂŒssel *)
Text: String;
(* <weitere Infokomponenten> *)
END;
ObjFTyp= ARRAY [1..MaxN] OF ObjTyp;
ObjLPtr= POINTER TO ObjLTyp;
ObjLTyp= RECORD (* Schlangenelement *)
Obj : ObjTyp;
Next : ObjLPtr;
END;
STyp = RECORD (* Schlangen-, Listentyp*)
Anfang : ObjLPtr;
Ende : ObjLPtr;
END;
VAR N : CARDINAL; (* Anzahl zu sortierender Records *)
TF : ObjFTyp; (* Feld mit Testdaten *)
TL : STyp; (* Liste mit Testdaten *)
p : ObjLPtr; (* Hilfszeiger *)
i : CARDINAL;
(**********************************************
* Element an eine lineare Liste anhÀngen *
* Dies entspricht dem EinfĂŒgen in eine *
* Schlange *
**********************************************)
PROCEDURE LLAnhaengen(VAR L:STyp; P:ObjLPtr);
(* Element, auf das 'P' zeigt, an die lineare
Liste 'L' hÀngen. Diese Operation ent-
spricht dem EinfĂŒgen in eine Schlange.
*)
BEGIN
IF P <> NIL THEN
P^.Next:=NIL; (* neues Listenende markieren *)
IF L.Anfang=NIL THEN (* Liste ist leer *)
L.Anfang:=P;
ELSE (* Liste war nicht leer *)
L.Ende^.Next:=P;(* Element anhÀngen, auf das P zeigt *)
END;
L.Ende:=P; (* Zeiger auf das Listenende umbiegen *)
END (* IF *)
END LLAnhaengen;
(**********************************************
* Zwei Listen verbinden *
**********************************************)
PROCEDURE LLVerbinden(VAR L1, L2: STyp);
(* Liste 'L2' wird an Liste 'L1' gehÀngt. Die
Ergebnisliste ist 'L1'.
*)
BEGIN
IF L1.Anfang=NIL THEN (* 1. Liste ist leer *)
L1:=L2;
ELSIF L2.Anfang<>NIL THEN (* 2. Liste nicht leer *)
L1.Ende^.Next:=L2.Anfang; (* verbinden *)
L1.Ende:=L2.Ende
END;
END LLVerbinden;
(**********************************************
* Liste ausgeben *
**********************************************)
PROCEDURE LLAusgeben(L : STyp);
VAR p : ObjLPtr;
BEGIN
p:=L.Anfang;
WHILE p<>NIL DO
Write(p^.Obj.Key);
WriteString(" ");
WriteString(p^.Obj.Text);
WriteLn;
p:=p^.Next;
END(*WHILE*);
WriteLn;
END LLAusgeben;
(**********************************************
* *
* BehÀltersortieren *
* *
**********************************************)
PROCEDURE BinSort(VAR S : STyp);
VAR B : ARRAY KeyTyp OF STyp; (* Feld mit BehÀltern *)
p : ObjLPtr; (* Hilfszeiger *)
i : KeyTyp; (* Laufvariable *)
BEGIN
(* BehÀlter löschen *)
FOR i:=MinKey TO MaxKey DO
B[i].Anfang:=NIL;
B[i].Ende:=NIL;
END;
(* Schlange S in BehÀlter sortieren *)
(* Es wird jeweils der Listenanfang von S
in einen BehÀlter einsortiert.
LLAnhaengen zerstört den Next-Zeiger
des Listenanfangs. Deshalb muss ein Zeiger
auf das jeweils zweite Listenelement
zwischengespeichert werden.
*)
WHILE S.Anfang<>NIL DO
p:=S.Anfang^.Next; (* Zeiger auf nÀchstes Element retten *)
LLAnhaengen(B[S.Anfang^.Obj.Key],S.Anfang);
S.Anfang:=p;
END;
(* BehÀlter auflösen und sortierte Liste generieren *)
S.Anfang:=NIL;
S.Ende:=NIL;
FOR i:=MinKey TO MaxKey DO
IF B[i].Anfang<>NIL THEN (* Schlange in BehÀlter B[i] nicht leer => verbinden *)
LLVerbinden(S,B[i]);
END;
END;
END BinSort;
(**********************************************
* Eine lineare Liste aus einem Feld mit *
* Daten generieren. *
**********************************************)
PROCEDURE ErzeugeListeAusFeld(F : ObjFTyp; VAR L : STyp);
(* Die Liste L wird zurĂŒckgegeben *)
VAR p : ObjLPtr; (* Hilfszeiger *)
i : CARDINAL;
BEGIN
L.Anfang:=NIL;
L.Ende:=NIL;
FOR i:=1 TO N DO
Allocate(p,SIZE(ObjLTyp));
p^.Obj.Key:=F[i].Key;
p^.Obj.Text:=F[i].Text;
LLAnhaengen(L,p);
END(*FOR*);
END ErzeugeListeAusFeld;
BEGIN (* Hauptprogramm *)
(* Feld mit Testdaten initialisieren *)
TF[1].Key:="B"; TF[1].Text:="Bruno";
TF[2].Key:="A"; TF[2].Text:="Anna";
TF[3].Key:="K"; TF[3].Text:="Klaus";
TF[4).Key:="F"; TF[4].Text:="Fritz";
TF[5].Key:="K"; TF[5].Text:="Kuni";
TF[6].Key:="H"; TF[6].Text:="Heike";
TF[7].Key:="F"; TF[7].Text:="Franz";
TF[8].Key:="Z"; TF[8].Text:="Zenzi";
TF[9].Key:="B"; TF[9].Text:="Bernd";
TF[10].Key:="E"; TF[10].Text:="Emil";
N:=10;
(* Liste mit Testdaten generieren *)
ErzeugeListeAusFeld(TF,TL);
LLAusgeben(TL);
BinSort(TL); (* Liste sortieren *)
LLAusgeben(TL);
END Listing4.
'********************************************
'* Listing 5 *
'* Sven KrĂŒppel, 2.1.91, (c) MAXON Computer *
'********************************************
DEF PROC Ll_Verbinden(R Anfang1%,R Ende1%,Anfang2%,Ende2%)
'HĂ€ngt die durch Anfang2% und Ende2%
'definierte Liste an die durch Anfang1% und
'Ende1% definierte Liste.
IF Anfang1%=0 THEN ' 1. Liste ist leer.
Anfang1%=Anfang2% ' funktioniert auch,
Ende1%=Ende2% ' wenn beide leer
ELSE ' 1. Liste ist nicht leer
IF Anfang2%<>0 THEN '2. Liste nicht leer
Wl_Next%(Ende1%)=Anfang2%' verbinden
Ende1%=Ende2%
ENDIF
ENDIF
RETURN 'Ll_Verbinden