home *** CD-ROM | disk | FTP | other *** search
-
-
- Abbildung 1:
- Ein Trie mit sieben Worten.
-
- Abbildung 2:
- Der gleiche Trie wie in Abbildung 1, aber in der
- speicherplatzsparenden Repräsentation.
-
-
- Der Trie
-
- Datenstruktur zur Speicherung von Wörterbüchern
-
- von Martin Holzherr
-
-
- Wir stellen Ihnen die effizienteste Methode vor, Wörterbücher zu
- speichern: Der Trie. Mit den erarbeiteten Algorithmen läßt sich,
- quasi als Abfallprodukt, auch ein leistungsfähiger
- Crossreferenzer zur Analyse von Prozeduren bauen unter
- Berücksichtigung global deklarierter Variable.
-
- Unter einem "Trie" versteht man einen Vielwegbaum, in dessen
- Knoten einzelne Buchstaben eines Wortes gespeichert sind. Folgt
- man von der Wurzel des Trie allen möglichen Pfaden bis zu seiner
- Wortendmarke, erhält man alle im Tree gespeicherten Worte.
-
- Abbildung 1 zeigt einen Trie, in dem die Worte BALL, BAR, BAU,
- BAUM, BIER, BILD und IOD gespeichert sind.
-
- Die Datenstruktur des Trie ist schon lange bekannt, wird jedoch
- viel zu selten eingesetzt, obwohl der Trie die effizienteste
- Datenstruktur ist, um zum Beispiel ein alphabetisches
- Verzeichnis aller Worte in einem Textfile zu erhalten. Eine
- detailierte Beschreibung des Trie als abstrakter Datentyp findet
- sich in dem Buch "Algorithmen und Datenstrukturen" (1).
-
- Will man nun diese Datenstruktur in einer Hochsprache wie
- Pascal, Modula oder C implementieren, taucht sofort eine
- Schwierigkeit auf: Beschränkt man sich auf die Großbuchstaben
- des Alphabetes, kann jeder Trie-Knoten bis zu 26 Nachfolger
- haben. Praktisch wird dieses Maximum jedoch nie erreicht. Die
- folgende Deklaration eines Trieknotens bedeutet deshalb eine
- enorme Speicherplatzverschwendung:
-
- ,,l
- TYPE TRIE = POINTER TO NODE
- NODE = RECORD
- Buchstabe :CHAR;
- Wortende :BOOLEAN;
- Nachfolger:ARRAY["A".."Z"] OF TRIE
- END;
-
- Angenommen ein Pointer benötige vier Byte, dann würde ein
- einziger, nach diesem Schema deklarierter Trieknoten 106 Byte
- belegen. Der Speicherbedarf der Zeiger ist gegenüber den anderen
- Informationen, dem Zeichen mit einem Byte und dem "Wortende" mit
- 2 Byte, enorm groß.
- Daher ist es zur Einsparung von Speicherplatz günstiger, einen
- Binärbaum als Vielwegbaum zu "mißbrauchen". Anstatt einen linken
- und einen rechten "Sohn" wie bei einem gewöhnlichen Binärbaum
- hat hier jeder Knoten einen Horizontalzeiger zu einem
- "Geschwisterknoten" und einen Vertikalzeiger zum ersten
- "Nachfolgerknoten". Dazu gehört folgende Deklaration:
-
- ,,l
- TYPE TRIE = POINTER TO NODE
- NODE = RECORD
- Buchstabe :CHAR;
- Wortende :BOOLEAN;
- horizontal, vertikal: TRIE
- END;
-
-
- ,,z
- Der Aufbau eines Trie-Wörterbuches
-
-
- Ein naheliegendes Verfahren, um ein alphabetisches Verzeichnis
- aller Wörter eines Textfiles zu erhalten, wäre, das File
- wortweise zu lesen und immer, wenn der letzte Buchstabe eines
- Wortes erkannt ist, dieses in das Wörterbuch einzufügen. Der
- Vorteil des Trie gegenüber anderen Datenstrukturen ist es nun
- aber gerade, daß eine solche Zwischenspeicherung von ganzen
- Worten gar nicht nötig ist. Wir stellen im folgenden eine
-
- PROCEDUREinsert(VAR t:TRIE);
-
- vor, die viel effizienter und eleganter arbeitet: Jedesmal, wenn
- der erste Buchstabe eines Wortes gelesen wurde, wird insert
- aufgerufen und zu dem Trieknoten verzweigt, der diesem ersten
- Buchstaben entspricht.
-
- Dann erst wird der nächste Buchstabe gelesen und derjenige
- Nachfolgerknoten gesucht, der diesem Buchstaben entspricht.
- Falls kein solcher Nachfolger existiert, wird ein neuer Knoten
- geschaffen. Dieses Verfahren wird so lange weitergeführt, bis
- der letzte Buchstabe des Wortes gelesen und das Wort somit an
- der richtigen Stelle im Wörterbuch eingetragen ist. Der nun
- folgende Pseudocode beschreibt die notwendigen Schritte beim
- Einfügevorgang:
-
- (* Beim ersten Aufruf von insert sei der
- erste Buchstabe des Wortes bereits gelesen.
- Das gerade aktuell gelesene zeichen sei "ch".
- TRIE ist gemäß der platzsparenden
- Deklaration vereinbart *)
-
- Prozedur insert(VAR t:TRIE);
- BEGIN
-
- falls t auf NIL zeigt,
- dann alloziere Speicherplatz für einen neuen Knoten,
- speichere darin "ch" und laß t auf diesen neuen Knoten
- zeigen.
- ,,krek lies das nächste Zeichen "ch".
- falls es sich bei "ch" um einen Buchstaben handelt,
- dann plaziere ihn am richtigen Ort im Trie durch
- Aufruf von insert(t^.vertical)
- sonst: t^.Wortende:=TRUE
-
- ,,ffalls t nicht auf NIL zeigt, sondern auf einen Knoten mit
- einem Buchstaben, der alphabetisch nach "ch" kommt
- dann füge "ch" vor diesem Knoten in den Trie ein,
- indem du einen neuen Knoten q bildest, darin "ch"
- abspeicherst und den Horizontalzeiger von q auf
- auf den gleichen Ort richtest auf den t zeigt,
- anschließend soll t neu auf q zeigen.
- führe die gleichen Aktionen aus, wie unter
- ,,krek beschrieben.
-
- ,,ffalls t nicht auf NIL zeigt, sondern auf einen
- Knoten mit einem Buchstaben, der alphabetisch vor
- "ch" kommt
- dann füge "ch" nach diesem Knoten in den Trie ein
- durch Aufruf von ,,finsert(t^.horizontal).
-
- ,,ffalls t nicht auf NIL zeigt, sondern auf einen
- Knoten mit demselben Buchstaben wie "ch"
-
- dann führe die Aktionen durch, die unter ,,krek
- beschrieben sind.
- END insert;
-
- Wie Sie sehen, arbeitet die Prozedur insert rekursiv. Um das
- fertige Wörterbuch schließlich alphabetisch auszugeben, bietet
- sich ebenfalls eine rekursive Prozedur an.
-
- Diese Prozedur mit dem Kopf
- ,,l
- PROCEDURE printTrie(t:TRIE;level:INTEGER);
-
- verwendet ein global deklariertes Zeichenfeld S um den
- Anfangsteil eines Wortes zu speichern. Beim ersten Aufruf von
- printTrie muß level = 0 sein. printTrie traversiert den Trie und
- speichert die gefundenen Buchstaben im Feld S, so daß
- S[0..level] jeweils dem Anfang eines Wortes entspricht. Sobald
- eine Wortendmarke gefunden wird, wird S[0..level] ausgedruckt
- und anschließend die Traversierung fortgesetzt.
-
- Ein Beispiel soll die Arbeitsweise der beiden Prozeduren
- verdeutlichen. Der eingegebene Textstring "Welches Wort mit mehr
- als sechs Buchstaben kommt in dieser TOOLBOX - Ausgabe mehr als
- 50 mal vor" ergibt folgende Bildschirmausgabe:
-
- ALS
- AUSGABE
- BUCHSTABEN
- DIESER
- IN
- KOMMT
- MAL
- MEHR
- MIT
- SECHS
- TOOLBOX
- VOR
- WELCHES
- WORT
-
- Implementation der Algorithmen und
- Anwendungsbeispiele
-
-
- Mit der vorgestellten rekursiven Implementation lassen sich
- keine Geschwindigkeitsweltrekorde brechen. Dazu wären eine
- iterative Versionen der Prozeduren insert und ,,fprintTrie
- besser geignet. Aber eine rekursiv definierte Datenstruktur wie
- der Trie wird am einfachsten mit rekursiven Mitteln bearbeitet.
- Hier gilt nämlich: iterativ geht meistens schief.
-
- Wenn man die gegebene Implementation zum Beispiel verwendet, um
- einen Buchindex herzustellen, sollte im Trieknoten noch ein
- weiteres Feld vorgesehen sein, um die Seitenzahlen zu speichern.
- Noch besser wäre ein Feld mit einem Zeiger auf eine Liste von
- Seitenzahlen. Der Algorithmus würde dann zu jedem Wort im Text
- automatische eine Liste der Seitenzahlen erstellen, auf denen
- die Wörter vorkommen. Natürlich müßte dann eine Selektion
- vorgesehen werden, um nur signifikante Wörter zu erfassen. Die
- Seitenzahlen, auf denen das Wort "die" auftaucht sind mit
- Sicherheit irrelevant.
-
- Wir haben noch eine andere Anwendung des Trie ausgearbeitet: Ein
- Programm, das Prozeduren von Modula-Programmen analysiert und
- alle Bezeichner herausschreibt, die in einer Prozedur verwendet
- werden, dort aber nicht deklariert sind. Solche global
- deklarierten Bezeichner können nämlich vor allem bei
- Programmmodifikationen die Ursache für schwer zu entdeckende
- Fehler sein.
-
- Das Modul TrieCrossRef schreibt alle Variablen-, Typen-,
- Konstanten-, und Prozedurdeklarationen des zu analysierenden
- Programms auf ein File. Zusätzlich werden unter jedem
- Prozedurkopf die Bezeichner aufgelistet, die nicht im
- Deklarationsteil der Prozedur auftauchen.
-
- Die Vorgehensweise ist dann folgende: Zuerst werden alle Modula-
- Schlüsselwörter, also die reservierten Bezeichner wie zum
- Beispiel BEGIN FOR, etc., von dem File in einen Trie eingelesen.
- Diese Wörter werden einerseits gebraucht, um sie im Quelltext zu
- erkennen und, da uninteressant, einfach überlesen zu können.
- Andererseits braucht man sie, um Anfang und Ende von
- Prozedurkopf und -körper zu finden und den Verarbeitungsprozeß
- an diesen Stellen vom Einsammeln und Markieren von deklarierten
- Bezeichnern auf das Suchen von nicht deklarierten Bezeichnern
- umzuschalten.
-
- Für die eigentliche Programmanalyse benötigt man ein ganzes
- Array von Tries. Dieses Array sollte mindestens so viele Felder
- besitzen wie die maximale Verschachtelungstiefe der Prozeduren
- im zu analysierenden Programm. Der Index "triesPos" zeigt zu
- Beginn auf das erste Feld dieses Arrays. Jedesmal, wenn das
- Schlüsselwort PROCEDURE erkannt wurde, wird "triesPos" um eins
- erhöht und em Feld mit diesem Index wird zur Initialisierung
- eine Kopie des Tries mit den Schlüsselwörtern übergeben.
-
- Dann werden alle weiteren Worte, die zwischen den
- Schlüsselworten PROCEDURE und BEGIN oder PROCEDURE und PROCEDURE
- liegen, was einer eingeschachtelten Prozedur entsprechen würde,
- in diesen Trie aufgenommen und mit einer Marke versehen. Bei
- allen Bezeichnern, die nun zwischen den Schlüsselwörtern BEGIN
- und dem Ende der Prozedur gefunden werden, aber nicht im
- dazugehörigen Trie vermerkt sind, muß es sich nun um Bezeichner
- handeln, die nicht lokal deklariert sind.
-
- Diese Analyse wird durch folgende Eigenheiten der Sprache Modula
- etwas erschwert: das Schlüsselwort PROCEDURE kann nämlich nicht
- nur einen Prozedurkopf einleiten, sondern es kann auch
- Bestandteil einer Typendeklaration sein (Prozedurtypen,
- Prozedurvariable). TrieCrossRef erkennt diesen fall daran, daß
- dem Schlüsselwort PROCEDURE ein "=" beziehungsweise ein ":"
- vorrausgeht. Ferner müssen natürlich Kommentare, auch
- geschachtelte, und Strings übersprungen werden.
-
- Als zusätzliches "Feature" erhält jeder Prozedurkopf auf dem
- Ausgabefile eine Laufnummer, die die Position und
- Verschachtelungstiefe der Prozedur angibt. "-2-1" zum Beispiel
- bedeuted erste Unterprozedur der zweiten Prozedur des Programms.
- Natürlich wird auch jede geschachtelte Prozedur eingerückt.
- Aus Platzgründe müssen wir uns an dieser Stelle auf Andeutungen
- des Algorithmus beschränken, eine konkrete Implementation finden
- Sie auf der Spezial-Diskette, die zu dieser Beilage angeboten
- wird.
- (ms)
-
- ,,x
- Literatur:
- (1) Aho, Hopcroft, Ullman, Data structures and algorithms, 1983