← ST-Computer 03 / 1991

Datenstrukturen & Algorithmen in Omikron.BASIC und Modula 2, Teil 3: BehÀltersortieren - Radixsort

Grundlagen

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

  1. unsortiertes Feld A in Schlange S ĂŒbertragen
  2. Schlange S sortieren
  3. 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
Sven KrĂŒppel