Leibniz Universität Hannover Institut für Praktische Informatik Fachgebiet Programmiersprachen und Übersetzer Gerichtete azyklische Graphen und Suffix-Bäume von Dennis Jaschniok Bachelorarbeit Prüfer: Prof. Dr. Rainer Parchmann Zweitprüfer: Prof. Dr. Udo Lipeck Abgabedatum: 07.09.2009 Erklärung der Selbstständigkeit Hiermit versichere ich, dass ich die vorliegende Bachelorarbeit selbständig und ohne fremde Hilfe verfasst und keine anderen als die in der Arbeit angegebenen Quellen und Hilfsmittel verwendet habe. Die Arbeit hat in gleicher oder ähnlicher Form noch keinem anderen Prüfungsamt vorgelegen. Hannover, den 15. Oktober 2009 Dennis Jaschniok 2 Inhaltsverzeichnis 3 1 Einleitung 1.1 Motivation In der Zeichenkettensuche ist ein typisches Problem das Suchen nach einem Auftreten einer bestimmten Zeichenkette (eines so genannten Pattern) in einem Text. Für diese Suche existieren verschiedenste Algorithmen, die durch Vorverarbeitung des Pattern eine schnelle Suche erlauben. In der Bio-Informatik, die in den letzten Jahren immer stärker an Bedeutung gewonnen hat, beschäftigt man sich mit der Suche in DNA-Sequenzen. Diese Sequenzen haben eine sehr große Länge (z.T. mehr als eine Million Zeichen) und müssen häufig nach mehreren verschiedenen Pattern durchsucht werden. In diesem Fall wird man mit dem Problem konfrontiert, dass die Zeit für jede Suche nach einem Auftreten des Pattern immer noch proportional zur Länge des Textes ist. Gerade beim häufigen Suchen in z.B. Datenbanken sind die einfachen Such-Algorithmen deswegen ungünstig [?]. Besser wäre es in diesem Fall, den Text im Vorfeld zu analysieren und eine Datenstruktur zu benutzen, die später eine Suche nach dem Pattern in einer Zeit proportional zur Patternlänge, nicht mehr zur Textlänge, ermöglicht. Zwei solche Datenstrukturen sind Suffix-Bäume und gerichtete azyklische (Wort-) Graphen (auch DAGs oder DAWGs genannt), auf die im Folgenden in dieser Arbeit näher eingegangen wird. 1.2 Gliederung und Schwerpunkte der Arbeit Die vorliegende Arbeit ist im Wesentlichen in 7 Bereiche eingeteilt. Kapitel 1 stellt die Einleitung in das Thema dar. Neben der Motivation in Abschnitt 1.1 und der Gliederung sowie den Schwerpunkte der Arbeit in diesem Kapitel werden in Abschnitt 1.3 auch einige grundlegende Definitionen getroffen. Diese sind für das Verständnis der Arbeit erforderlich und sollen eine Art Grundlage für spätere Definitionen bilden. Kapitel 2 beschäftigt sich mit den Suffix-Tries, einer Datenstruktur, die als Grundlage für Suffix-Bäume und gerichtete azyklische Wortgraphen betrachtet werden kann. Insbesondere steht hier die Definition der Suffix-Tries, wie sie in späteren Kapiteln verwendet wird. Kapitel 3 befasst sich näher mit den Suffix-Bäumen. Zunächst wird in Abschnitt 3.1 auf die Struktur eingegangen, die Beziehung zu Suffix-Tries beschrieben sowie die formale 4 Definition angegeben. Hier wird außerdem der wichtige Satz 3.2 über die Knotenzahl angegeben und bewiesen. Für die Konstruktion eines Suffix-Baumes existieren eine Reihe von Algorithmen, von denen die wichtigsten in Abschnitt 3.2 erläutert werden. Abschnitt 3.3 beschäftigt sich mit Nachteilen der Suffix-Bäume und geht speziell auf eine Alternative, die Suffix-Arrays, ein. Zudem wird die Überleitung zu den gerichteten azyklischen Graphen im folgenden Kapitel gebracht. Kapitel 4 stellt den Schwerpunkt der Arbeit dar. Hier werden die gerichtete azyklischen Wortgraphen oder DAWGs vorgestellt. Dazu werden in 4.1 zunächst einige Definitionen und Lemmata aufgestellt und bewiesen, die schließlich zu der formalen Definition 4.8 eines gerichteten azyklischen Wortgraphen führen. Anschließend werden verschiedene Eigenschaften der DAWGs beschrieben und bewiesen. Abschnitt 4.2 beschäftigt sich mit einer Möglichkeit, DAWGs aus Suffix-Bäumen zu konstruieren. Der Konstruktionsalgorithmus wird in diesem Kapitel beschrieben und es wird zu diesem Zweck auch kurz auf die besondere Struktur von kompakten DAWGs eingegangen. Abschnitt 4.3 beschreibt einen Algorithmus, der DAWGs unmittelbar aus einem Text erzeugt. Dieser Abschnitt ist für die Implementierung im letzten Kapitel von besonderer Bedeutung, da der hier vorgestellte Algorithmus später implementiert wird. Entsprechend wird hier ausführlich auf die Korrektheit und die einzelnen Schritte zur Konstruktion eingegangen. Abschnitt 4.4 beschäftigt sich mit Beziehungen zwischen Suffix-Bäumen und DAWGs und beschreibt, wie man die Strukturen ineinander umwandeln kann. Abschnitt 4.5 erläutert zum Abschluss des Kapitels zwei Algorithmen, die DAWGs zur Zeichenkettensuche verwenden. Um die theoretischen Ansätze zu DAWGs an konkreten Texten testen zu können, wird in Kapitel 5 eine Implementierung dieser Datenstruktur vorgestellt. Das Ziel soll es dabei sein, Tests mit verschiedenen Texten durchzuführen und einen Vergleich mit einer Implementierung von Suffix-Bäumen hinsichtlich von Laufzeit und Speicherplatzbedarf anzustellen. In den einzelnen Abschnitten werden hier die Implementierungsdetails beschrieben und die programmierten Klassen vorgestellt. In Kapitel 6 werden die Ergebnisse der Messungen aus Kapitel 5 dargestellt und ausgewertet. Dabei werden drei Arten von Texten für die Tests verwendet, deren Messergebnisse jeweils gesondert in den Abschnitten 6.1, 6.2 und 6.3 dargestellt werden. Im letzten Kapitel, Kapitel 7 wird ein Fazit getroffen sowie über den Ausblick dieser Arbeit gesprochen. 1.3 Grundlegende Definitionen Für das Verständnis dieser Arbeit ist es nötig, vorab einige Begrifflichkeiten zu klären, die im Folgenden des öfteren verwendet werden. 5 Definition 1.1 Sei Σ ein nicht-leeres, endliches Alphabet und Σ∗ die Menge aller Wörter über dem Alphabet. bezeichnet dabei das leere Wort. Sei w ein Wort in Σ∗ . Dann • bezeichnet |w| die Länge von w • bezeichnet w[i . . . j] für 1 ≤ i ≤ |w|, 0 ≤ j ≤ |w| und j ≥ i das Teilwort von w, das an der i-ten Stelle beginnt und an der j-ten Stelle endet. Ist i > j, so ist w[i . . . j] das leere Wort . • heißt w[1 . . . j] für 0 ≤ j ≤ |w| Präfix von w, w[j . . . |w|] mit 1 ≤ j ≤ |w| + 1 Suffix von w. • heißt ein Präfix u von w echtes Präfix, wenn u 6= w gilt. • heißt ein Suffix v von w echtes Suffix, wenn v 6= w gilt. • ist w = an das Wort, das nur aus dem Buchstaben a besteht, der n-mal hintereinander gehängt wurde (also z.B. w = aaa für w = a3 ). • ist tail(w) das längste Suffix von w, das mehr als einmal in w auftritt (also z.B. tail(abcbc) = bc, tail(aab) = ) 6 2 Suffix-Tries Bevor in den folgenden Kapiteln näher auf die Suffix-Bäume und DAWGs eingegangen werden kann, ist es notwendig, zunächst die ihnen zu Grunde liegende Struktur, die Suffix-Tries, zu betrachten. Der Begriff des „Trie“ leitet sich aus dem Begriff „Information Retrieval“ (Wiederauffinden von Information) her. Hierzu zunächst wiederholend eine Definition zu Bäumen Definition 2.1 Ein Baum ist ein gerichteter Graph B = (V, E), wobei V die Menge der Knoten angibt, E die Menge der Kanten. Ferner gilt für den Baum: 1. Im Baum B gibt es genau einen Knoten w ∈ V mit (v, w) ∈ / E für alle v ∈ V , das heißt es gibt keine eingehenden Kanten in den Knoten w. w heißt die Wurzel von B. 2. Für alle v ∈ V, v 6= w existiert genau ein u 6= v mit (u, v) ∈ E, das heißt jeder Knoten ungleich der Wurzel hat genau eine eingehende Kante. 3. Für alle v ∈ V gibt es einen eindeutig bestimmten Weg von der Wurzel w nach v. Die Länge dieses Weges heißt Tiefe oder Stufe von v. Für einen solchen Baum nach Definition 2.1 gelten wie üblich die Begriffe: innerer Knoten, Blatt, direkter Vorgänger/Nachfolger, gemeinsamer Vorgänger und tiefster gemeinsamer Vorgänger zweier Knoten. Diese sind an anderer Stelle definiert und sollen hier nicht erneut aufgeführt werden. [?] Ein Trie stellt einen Baum dar, dessen Kanten mit einzelnen Symbolen beschriftet sind. Er wird wie folgt definiert: Definition 2.2 Ein Trie über einem Alphabet Σ ist ein Baum (V, E) mit einer Markierungsfunktion µ : E → Σ , die jeder Kante ein Symbol aus Σ zuordnet. Außerdem gilt: Sind (u, v1 ) ∈ E und (u, v2 ) ∈ E mit v1 6= v2 , dann ist µ(u, v1 ) 6= µ(u, v2 ), das heißt zwei von einem Knoten ausgehende Kanten haben niemals dieselbe Markierung. 7 Definition 2.3 Sei T = (V, E, µ) ein Trie. Jeder Knoten v ∈ V stellt ein Wort ρ(v) ∈ Σ∗ dar, das durch Konkatenation — also Aneinanderreihen — aller Markierungen der Kanten auf dem Weg von der Wurzel zum Knoten v gebildet wird. Dabei gilt ρ(v) = genau dann wenn v die Wurzel von T ist. Im Kontext dieser Arbeit wird der Trie stets für ein Wort bzw. für eine Menge von Wörtern konstruiert. Formal gesehen gilt: Definition 2.4 Sei P = w1 , . . . , wk eine Menge von Wörtern über Σ. Dann sei der Trie für P der kleinste Trie mit der Eigenschaft, dass es k Knoten u1 , . . . , uk im Trie gibt mit ρ(ui ) = wi , 1 ≤ i ≤ k. Ein Suffix-Trie stellt eine besondere Form eines Trie dar. Es gilt Definition 2.5 Ein Suffix-Trie für ein Wort w ∈ Σ∗ ist ein Trie für die Menge P aller Suffixe von w. In dieser Definition ist nicht ausgeschlossen, dass Suffixe des Wortes w durch interne Knoten repräsentiert werden. Um zu gewährleisten, dass jedes Suffix von w eindeutig einem Blatt des Tries zugeordnet werden kann, wird häufig gefordert, dass die Menge aller Suffixe präfix-frei ist, was bedeutet, dass kein Suffix von w Präfix eines anderen Suffix ist. Um das zu erreichen, wird w um eine Endmarkierung $ ∈ / Σ erweitert. In Abbildung 2.1 ist ein Suffix-Trie für das Wort w = babaaabb$ dargestellt. Die Blätter sind mit dem Ziffern 1-9 beschriftet, welche die Startpositionen des Suffixes in w darstellen, das durch das jeweilige Blatt repräsentiert wird. Mit Suffix-Tries kann man das eingangs erwähnte Problem des Suchens in einem feststehenden Text lösen. Durch die Baumstruktur ist leicht feststellbar, ob ein Wort x ein Teilwort eines Wortes w ist. Der Suffix-Trie ist in einer Vorverarbeitungsphase für einen gegebenen Text erzeugbar und die Suche nach einem Pattern erfolgt in O(m), unabhängig von der Textlänge. Ferner lassen sich die Anzahl der Auftreten von x in w sowie die Länge des längsten gemeinsamen Teilwortes zweier Wörter bestimmen. Suffix-Tries haben aber einen entscheidenden Nachteil: Die Anzahl der Knoten ist im ungünstigsten Fall O(|w|2 ), für die Erzeugung der Knoten des Tries werden O(|w|2 ) Schritte benötigt. [?] Daraus resultiert die Notwendigkeit, Tries kompakter darzustellen, um so eine effizientere Methode zur Speicherung zu bekommen. Hierfür gibt es im Wesentlichen zwei Ansätze: 8 $ b a $ 8 a b $ 7 a a b a b a $ b a b b b a 9 $ b a a a b b a b $ 5 b $ 4 6 b 3 $ 2 b $ 1 Abbildung 2.1: Suffix-Trie für w = babaaabb$ 1. Ketten von internen Knoten, deren Ausgangsgrad 1 ist, können zusammengefasst werden. Dieser Ansatz führt schließlich zu den Suffix-Bäumen, die in Kapitel 3 näher behandelt werden. 2. Isomorphe Teilbäume, das heißt Teilbäume, die dieselben Knoten enthalten, können zusammengefasst werden, um eine kompaktere Darstellung zu erhalten. Auf diese Methode werden die gerichteten, azyklischen Wortgraphen, wie sie in Kapitel 4 behandelt werden, zurückgeführt. Ein Algorithmus zur Zeichenkettensuche, der Tries verwendet, ist der Aho-Corasick Algorithmus. 9 3 Suffix-Bäume 3.1 Grundlegende Struktur von Suffix-Bäumen Wie bereits in Kapitel 2 erwähnt, haben Suffix-Tries einen erheblichen Nachteil hinsichtlich des Konstruktionsaufwands und Speicherbedarfs. Suffix-Bäume sind im Prinzip eine Erweiterung von Suffix-Tries, in denen man versucht, durch geschickte Vereinfachungen bessere Laufzeiten und einen geringeren Speicherbedarf zu erreichen. Hat man in Suffix-Tries noch häufig lange Ketten aus Knoten mit einem Ausgangsgrad von 1, also mit nur einer ausgehenden Kante, werden diese bei Suffix-Bäumen zusammengefasst. Formal lässt sich ein Suffix-Baum wie folgt beschreiben: Definition 3.1 Ein Suffix-Baum (V, E, µ) für ein Wort w ∈ Σ∗ ist ein Baum (V, E) mit der Markierungsfunktion µ : E → Σ+ , für den gilt: – Sind (u, v1 ) ∈ E und (u, v2 ) ∈ E mit u, v1 , v2 ∈ V , u1 6= u2 , dann ist das erste Symbol von µ(u, v1 ) ungleich dem ersten Symbol von µ(u, v2 ). – Der Baum hat genau |w| Blätter, die mit 1, . . . , |w| markiert sind. – Für |w| > 1 hat jeder interne Knoten im Suffix-Baum einen Verzweigungsgrad ≥ 2. – Jeder Knoten u repräsentiert ein Wort ρ(u). Ist u die Wurzel, so ist ρ(u) = . Ist andernfalls (u, v) ∈ E, so ist ρ(v) = ρ(u)µ(u, v). – Ein Blatt i repräsentiert das Suffix von w, das an Position i beginnt, es gilt also ρ(i) = w[i..|w|]. Die Definition besagt, dass innerhalb eines Suffix-Baumes alle von einem Knoten ausgehenden Kanten eindeutig beschriftet sein müssen. Die Kanten können zwar durch Aneinanderreihungen von Zeichen beschriftet werden, man muss sie aber zwangsläufig am ersten Zeichen erkennen können. Abbildung 3.1 zeigt einen Suffix-Baum für das Wort w = babaaabb$. Er repräsentiert dieselben Worte wie der Suffix-Trie in Abbildung 2.1 aus dem vorigen Kapitel. 10 $ b 9 a $ 8 a b$ a b 7 aabb$ 3 baaabb$ 1 aaabb$ 2 b$ 6 bb$ abb$ 4 5 Abbildung 3.1: Suffix-Baum für w = babaaabb$ Jeder Knoten im Suffix-Baum repräsentiert ein Wort, das man durch Aneinanderreihung der Kantenmarkierungen auf dem Weg von der Wurzel zu dem Knoten bekommt. Ist der Knoten ein Blatt, dann repräsentiert er ein Suffix des Wortes w, für das der Suffix-Baum angelegt wurde. Voraussetzung dafür, dass ein Suffix-Baum zu einem Wort w ∈ Σ+ existiert, ist, dass die Menge aller Suffixe von w präfix-frei ist. Wie im vorigen Kapitel erwähnt, kann man das durch Anhängen eines neuen Symbols $ ∈ / Σ an w erreichen. Wenn nicht explizit anders erwähnt, wird im Folgenden immer davon ausgegangen, dass die Menge der Suffixe von w präfix-frei ist. Aufgrund der Definition des Suffix-Baumes lässt sich folgende Abschätzung treffen: Satz 3.2 Die Zahl der Knoten in einem Suffix-Baum für w ∈ Σ+ mit |w| = n und n > 1 ist durch 2n − 1 beschränkt. Beweis Nach der Definition hat ein Suffix-Baum genau n Blätter. Diese Blätter haben höchstens bn/2c direkte Vorgänger, die dann wiederum höchstens bn/4c direkte Vorgänger haben usw. Insgesamt ist die Zahl der internen Knoten also nach oben durch bn/2c + bn/4c + · · · ≤ n/2 + n/4 + n/8 + · · · ≤ n − 1 beschränkt. Zählt man zu dieser Zahl der internen Knoten noch die Blätter dazu, folgt unmittelbar der Satz. Für den Speicherplatz des Suffix-Baumes ist es ungünstig, die Kanten mit kompletten Teilworten, also Aneinanderreihungen von Zeichen, zu beschriften. In diesem Fall würde man für lange Ketten im Trie einen quadratischen Speicheraufwand für die zusammengefassten Kanten bekommen. 11 Da alle Kantenmarkierungen Teilworte des zu Grunde liegenden Wortes w sind, kann man die Kanten durch zwei Indizes beschreiben, die die Positionen in w angeben, an denen das Teilwort beginnt und endet. Eine Kante mit der Markierung (i1 , i2 ) symbolisiert also das Teilwort w[i1 . . . i2 ]. Da man so für jede Kante genau zwei Werte speichern muss, kann man diese Werte auch in den Knoten speichern und so vollständig auf Kantenmarkierungen verzichten. Abbildung 3.2 zeigt den Suffix-Baum für w = babaaabb$, in dem die Kantenmarkierungen durch die zwei Zahlenwerte repräsentiert werden. (9,9) (1,1) 9 (2,2) (9,9) 8 (2,2) (8,9) (5,5) (3,3) 7 (5,9) 3 (3,9) 1 (4,9) 2 (8,9) 6 (6,9) (7,9) 4 5 Abbildung 3.2: Suffix-Baum für w = babaaabb$ mit Indizes 3.2 Konstruktionsmöglichkeiten (Algorithmen) Es sollen nun einige Möglichkeiten vorgestellt werden, einen Suffix-Baum zu konstruieren. Dafür gibt es eine Vielzahl von Möglichkeiten, von denen die wichtigsten hier kurz angesprochen werden sollen. Nachdem zuvor die Verwandtschaft von Suffix-Bäumen zu Suffix-Tries erwähnt wurde, liegt es nahe, einen Suffix-Trie wie in Kapitel 2 zu erzeugen und aus diesem im Anschluss einen Suffix-Baum zu konstruieren. Dazu muss man lediglich alle Knoten mit einem Ausgangsgrad von 1 aus dem Baum entfernen und die zugehörigen Kantenmarkierungen zusammenfassen. Diese Art der Konstruktion ist denkbar einfach und leicht nachzuvollziehen, hat aber den entscheidenden Nachteil, dass man auf diese Weise immer den „Umweg“ über den Suffix-Trie gehen muss. Dadurch bekommt man einen quadratischen Algorithmus, also einen, der in O(n2 ) arbeitet und für den sowohl zusätzlicher Speicheraufwand als auch zusätzliche Zeit notwendig sind. Das führt zu der Überlegung, den Suffix-Baum direkt aus dem Wort zu konstruieren, um so eine deutliche schnellere und speicherplatzsparendere Möglichkeit zu finden. 12 Für diese direkte — oder on-line — Konstruktion sollen nun zunächst die Algorithmen von Weiner und McCreight vorgestellt werden, wie sie in [?] und [?] beschrieben werden. Dabei sollen Details beider Algorithmen nicht näher erläutert werden, da sie nicht Teil dieser Arbeit sein sollen. Die Beschreibungen sollen vielmehr einen Eindruck vermitteln, welche verschiedenen Möglichkeiten es prinzipiell zur Konstruktion von Suffix-Bäumen gibt. Beide Algorithmen arbeiten dabei grundsätzlich gleich. Der Baum für das Wort w wird schrittweise konstruiert, indem man die Suffixe von w nacheinander einfügt. McCreights Algorithmus fügt dabei zuerst das längste Suffix in den Baum ein, dann im nächsten Schritt das zweitlängste usw., während Weiner zunächst das kürzeste Suffix einfügt und anschließend schrittweise die längeren. McCreight beginnt mit einem Baum, der nur einen Knoten besitzt. Es werden nun nacheinander die Einfügepositionen für die Suffixe von w gesucht und entsprechend neue Knoten und Kanten erzeugt. Bei der Suche nach der Einfügeposition von w[i . . . n] läuft man einen Suchpfad von der Wurzel des Baumes beginnend ab, bis man eine Stelle erreicht hat, an der keine Kantenmarkierung mehr dem nächsten Buchstaben des aktuell einzufügenden Suffixes entspricht. Man hat an dieser Stelle also das Präfix ρ von w[i . . . n] gefunden, das Restwort α von w[i . . . n] muss noch eingefügt werden. Dabei können zwei Fälle auftreten: 1. Der Suchpfad endet in einem Knoten. In diesem Fall müssen eine neue Kante und ein neuer Knoten eingefügt werden, wobei die Kante mit α beschriftet wird. 2. Der Suchpfad endet innerhalb einer Kante. In diesem Fall muss dort, wo der Suchpfad endet, ein neuer Knoten erzeugt und die Kante aufgebrochen werden. Von dem neuen Knoten aus müssen zwei Kanten eingefügt werden, eine mit α und die andere mit der restlichen Markierung der aufgebrochenen Kante beschriftet. Um die Suche nach der Einfügeposition in linearer Zeit möglich zu machen, werden so genannte Suffix-Links eingefügt. Diese verweisen von einem Knoten, der ein Wort au repräsentiert, auf einen Knoten u, der einen Suffix des Wortes repräsentiert, wobei u ∈ Σ∗ , a ∈ Σ ist. Durch Folgen der Suffix-Links kann man auf diese Weise schnell im Baum nach dem Knoten suchen, der den längsten Präfix ρ von w[i . . . n] repräsentiert. Von diesem Knoten aus muss man unter Umständen noch einer Kante folgen, wenn w[i . . . n] innerhalb der Kante endet, um den eigentlichen repräsentativen Knoten zu erreichen. Es kann gezeigt werden, dass die komplette Suche und somit auch die gesamte Konstruktion nach dem Algorithmus in linearer Zeit möglich ist. Weiners Algorithmus arbeitet nach dem selben Schema und unterscheidet ebenfalls zwischen den beiden Fällen bei der Suche nach der Einfügeposition. Da die Suffixe vom 13 kürzesten beginnend eingefügt werden, werden hier statt Suffix-Links so genannte aLinks verwendet. Diese sind das genaue Gegenstück der Suffix-Links, das heißt, würde ein Suffix-Link von au nach u führen, führt der a-Link von u nach au. Auch bei Weiner kann gezeigt werden, dass die Konstruktion nach dem Algorithmus in linearer Zeit abläuft. Neben diesen beiden Algorithmen, die sehr ähnlich arbeiten, soll nun ein dritter vorgestellt werden, der eine direkte Konstruktion von Suffix-Bäumen erlaubt. Dieser Algorithmus stammt von Ukkonen aus dem Jahr 1992 und wird in [?] näher beschrieben. Auch hier soll nur auf das Prinzip eingegangen werden, da eine genaue Betrachtung den Rahmen dieser Arbeit sprengen würde. Grundsätzlich konstruiert Ukkonen seinen Algorithmus für einen impliziten Suffix-Baum. Das bedeutet, dass hier auf Präfix-Freiheit verzichtet wird und somit im Ergebnisbaum Suffixe auch durch interne Knoten repräsentiert werden können. Es ist aber möglich, am Ende des Algorithmus aus dem impliziten Suffix-Baum für w mit w ∈ Σ∗ den SuffixBaum für w$, $ ∈ Σ zu erzeugen, in dem alle Suffixe durch Blätter dargestellt werden. Der Algorithmus arbeitet in n Phasen, wobei jede Phase genau ein Zeichen aus w in den Suffix-Baum einfügt. Dabei wird in der Phase i das i-te Zeichen von w eingefügt. Hierbei unterscheidet er sich bereits von McCreight und Weiner, die beide jeweils ein komplettes Suffix in einem Schritt einfügen. Jede Phase ist in j Erweiterungsschritte eingeteilt, in denen alle Suffixe w[j . . . i] von w[1 . . . i] eingefügt werden. Für die Erweiterungen gibt es mehrere Möglichkeiten: 1. Der Suchpfad endet in einem Blatt. Dann muss die letzte Markierung auf dem Weg um das aktuelle Zeichen w[i] erweitert werden. 2. Der Suchpfad endet in einem internen Knoten. Dann muss ein neues Blatt eingefügt werden, das über eine Kante w[i] erreichbar ist. 3. Der Suchpfad endet innerhalb einer Kantenmarkierung. Dann muss die Kante an dieser Stelle aufgebrochen und ein neuer Knoten eingefügt werden, von dem aus eine Kante w[i] zu einem neuen Blatt führt. 4. Der Suchpfad endet so, dass es eine Fortsetzung mit w[i] gibt. In diesem Fall ist nichts zu tun. Auf diese Weise würde der Algorithmus in O(n3 ) ablaufen. Um ihn auf lineare Laufzeit zu bekommen, stellt Ukkonen eine Reihe von Möglichkeiten zur Verbesserungen vor. Dazu gehört die Einführung von Suffix-Links wie auch schon in McCreights Algorithmus, das frühzeitige Abbrechen einer Phase unter bestimmten Bedingungen, die implizite Ausführung von Schritt 1 und Einschränkung von Suffix-Erweiterungsschritten. Dadurch bekommt man schließlich eine Laufzeit von O(n). 14 3.3 Nachteile und Alternativen Obwohl nun gezeigt wurde, dass Suffix-Bäume auf verschiedene Arten in linearer Zeit konstruiert werden können, haben sie doch nach wie vor einen entscheidenden Nachteil: Hat man ein großes Alphabet gegeben und zusätzlich nur eine begrenzte Menge an Speicherplatz zur Verfügung, wird die Darstellung eines Wortes mit Hilfe eines SuffixBaumes schnell zu speicherplatzintensiv. Hinzu kommt noch, dass die Konstruktions-Algorithmen von Suffix-Bäumen recht komplex sein können. Betrachtet man beispielsweise den Ukkonen-Algorithmus, ist dieser intuitiv zunächst wenig verständlich und nicht gerade leicht zu implementieren. Man benötigt also Strukturen, die weniger Speicherplatz benötigen als die Suffix-Bäume und möglichst auf einfacherem Wege erzeugt werden können. Hierzu seien zunächst die Suffix-Arrays genannt. Diese ordnen die Suffixe eines Texten in lexikographischer Reihenfolge in einem Array an und ermöglichen so eine binäre Suche. w 1 b 2 a 3 b 4 c 5 c 6 b 7 a 8 a 9 c 10 b 11 $ Das dazu gehörende Array: A 11 7 2 8 10 6 1 3 9 5 4 $ a a a b b b b c c c a b c $ a a c b b c c c b b c $ $ b a a c b $ a b c $ a b c c b b c a $ b a a c a b c $ b a a c a b c $ b $ $ Suffix-Arrays haben den Vorteil gegenüber Suffix-Bäumen, dass sie sehr einfach konstruiert und verwendet werden können. Sie benötigen für die Konstruktion allerdings meist etwas mehr Zeit als die Suffix-Bäume, wenngleich hier auch kurz eine Möglichkeit vorgestellt werden soll, welche in linearem Zeitaufwand funktioniert. 15 Für die Konstruktion von Suffix-Arrays gibt es mehrere unterschiedliche Algorithmen. Es ist möglich, sie aus Suffix-Bäumen herzuleiten, indem man einen alphabetischen Tiefendurchlauf durch den Baum ausführt, das heißt die Kanten und Knoten in der lexikographisch aufsteigenden Reihenfolge per Tiefendurchlauf durchläuft. Auf diese Weise bekommt man in linearer Zeit das gesuchte Array. Der offensichtliche Nachteil daran ist, dass dazu zunächst ein Suffix-Baum benötigt wird, was die Laufzeit unnötig erhöht. Direkte Algorithmen, die ein Suffix-Array erzeugen, sind etwa von Manber und Myers (1993) oder von Ko und Aluru (2003) beschrieben worden, hier soll im Folgenden nur kurz auf das Prinzip beider Algorithmen eingegangen werden. Für Details sei hier auf die jeweilige Primärliteratur verwiesen, wie sie an entsprechender Stelle sowie im Literaturverzeichnis angegeben ist. Manber und Myers beschreiben in [MM 93] einen Algorithmus, der nach dem Prinzip von Bucket-Sort arbeitet. Dabei wird zu Beginn ein Array erzeugt und die einzelnen Positionen durchnummeriert, anschließend werden im ersten Iterationsschritt die Suffixe anhand ihrer ersten Buchstaben einsortiert. Dabei entstehen so genannte Buckets, die jeweils Suffixe enthalten, die mit denselben Buchstaben beginnen. Die Grenzen dieser Buckets werden dabei durch Zeiger markiert. In den nächsten Iterationsschritten werden nun jeweils doppelt so viele Zeichen wie im vorangehenden Schritt betrachtet und dementsprechend die Buckets weitersortiert. So sortiert man im zweiten Schritt nach den ersten zwei Buchstaben der Suffixe, im dritten dann nach den ersten vier Zeichen und so weiter, bis man alle Suffixe korrekt eingeordnet hat und das Suffix-Array fertig ist. Das ist für ein Wort w mit |w| = n spätestens nach log(n) Iterationsschritten der Fall, der gesamte Algorithmus läuft in O(n log n) ab. Ko und Aluru stellen in [KA 03] einen Algorithmus vor, der in linearer Zeit arbeitet. Dieser ist im Prinzip in drei Teile eingeteilt. Zunächst werden die Suffixe des übergebenen Wortes w ∈ Σ in zwei Typen eingeteilt, was in linearer Zeit möglich ist. Die Suffixe der Länge i werden dabei Typ S zugeordnet, wenn sie alphabetisch kleiner als die Suffixe der Länge i + 1 sind, ansonsten werden die Typ L zugeordnet. Bis auf das letzte Zeichen von w, das hier wieder ein $ ∈ / Σ darstellt, ist diese Zuordnung eindeutig. Im zweiten Teil des Algorithmus werden die Suffixe der jeweiligen Typen geordnet. Hierzu beschreiben Ko und Aluru eine Methode, die das in linearer Zeit im Array ermöglicht. Im letzten Schritt hat man nun die jeweils geordneten Typ-S- und Typ-L-Suffixe, die nun noch insgesamt sortiert werden müssen. Auch dazu stellen Ko und Aluru eine Möglichkeit vor, die das in linearer Zeit schafft. Insgesamt erhält man auf diese Weise einen Algorithmus, der das Suffix-Array in einer Zeit O(n) konstruiert. Eine weitere Struktur, die als Alternative von Suffix-Bäumen verwendet werden kann, ist der gerichtete azyklische Wortgraph oder DAWG. Diese Struktur soll im folgenden Kapitel intensiv betrachtet werden. 16 4 Gerichtete azyklische Wortgraphen (DAWGs) 4.1 Definition Gerichtete azyklische Wortgraphen - auch DAWGs genannt (aus dem Englischen: directed acyclic word graph) - lassen sich genauso wie Suffix-Bäume aus Suffix-Tries herleiten. Die generelle Verbesserung, verglichen mit den Suffix-Bäumen, ist das Zusammenfassen isomorpher Teilbäume, was eine kompaktere und somit speicher- und zeitsparendere Methode für die Zeichenkettensuche ermöglicht. Informell gesagt ist ein DAWG zu einem Wort w zunächst ein einfacher gerichteter Graph, der einen Startknoten (Quelle) und einen Endknoten (Senke) besitzt und dessen Knoten in bestimmter Weise miteinander verbunden sind. Dabei korrespondiert jeder Knoten mit einem bestimmten Teilwort aus w, wie weiter unten näher ausgeführt wird. Bevor auf die formale Definition der DAWGs eingegangen und somit die informelle Beschreibung spezifiziert werden kann, sollen zunächst die Begriffe des end-sets und der Isomorphie beschrieben werden. Außerdem wird im Folgenden stets von feststehenden Wörtern ausgegangen. Definition 4.1 Sei w = a1 · · · an mit a1 , . . . , an ∈ Σ ein Wort aus einem Alphabet Σ. Für ein nichtleeres Wort y ∈ Σ∗ ist das end-set von y in w gegeben durch end-setw (y) = {i : y = aj−|y|+1 · · · ai }. Dabei ist end-setw () = {0, 1, 2, . . . , n} Das end-set von y stellt also die Menge aller Positionen dar, an denen ein Auftreten von y innerhalb des Wortes w endet. Da sich im Folgenden, wenn nicht explizit anders erwähnt, das end-set immer auf ein Wort w bezieht, soll statt end-setw (y) die Schreibweise endset(y) äquivalent verwendet werden. Beispiel 4.2 Sei w = abcbc ein nicht-leeres Wort über einem Alphabet Σ. In diesem Fall gilt z.B. end-set(bc) = {3, 5} sowie end-set(a) = {1} 17 Definition 4.3 Zwei Worte x und y aus Σ∗ sind isomorph in w genau dann wenn end-set(x) = endset(y), in Zeichen x ≡w y. Mit [x]w sei im Folgenden die Äquivalenzklasse von x bezogen auf ≡w bezeichnet. Zwei Worte x und y sind also isomorph in w, wenn sie beide an der- oder denselben Positionen in w enden. In einem DAWG würde Isomorphie bedeuten, dass zwei Pfade x und y in demselben Knoten enden würden. Hieraus ist direkt ersichtlich, dass jeder Knoten in einem DAWG zu einer nicht-leeren Menge von end-sets gehört. Beispiel 4.4 Sei w = abcbc ein nicht-leeres Wort über einem Alphabet Σ. Seien außerdem x = bc und y = c zwei Worte aus Σ∗ . Dann gilt end-set(x) = {3, 5} = end-set(y) . Nach Definition gilt dann x ≡w y, das heißt x und y sind isomorph. Im folgenden Lemma sollen einige Eigenschaften von Isomorphie zusammengetragen werden. Lemma 4.5 1. Isomorphie ist eine rechts-invariante Äquivalenz-Relation über Σ∗ , d.h. für alle x, y, z ∈ Σ∗ impliziert x ≡ y auch xz ≡ yz. 2. Wenn zwei Wörter isomorph sind, dann ist das eine Suffix des anderen. 3. Zwei Wörter xy und y sind isomorph genau dann, wenn jedes Auftreten von y unmittelbar auf ein Auftreten von x folgt. Das folgende Beispiel soll diese Eigenschaften etwas näher verdeutlichen. Beispiel 4.6 Sei w = abcbc sowie x und y wie in Beispiel 4.4 gegeben. Sei außerdem b ∈ Σ∗ dasselbe Zeichen wie die b in w. Dann gilt wegen end-set(xb) = {4} = end-set(yb) offensichtlich auch xb ≡w yb (Punkt 1). Ebenso ist klar, dass hier y ein Suffix von x ist (Punkt 2) und dass jedes Auftreten von y unmittelbar auf ein Auftreten von x folgt (Punkt 3). In der Theorie könnte man die oben angegebene Definition von end-set benutzen, um einen DAWG zu konstruieren, dessen Knoten durch die komplette Menge von Positionen aus end-set bestimmt werden. In der Praxis hat die Verwendung der end-sets jedoch einen bedeutenden Nachteil: da die Menge der end-sets häufig groß ist, würde die Größe des DAWGs auf deutlich über linear steigen. 18 Um das zu verhindern, erlaubt man innerhalb des DAWGs Kantenmarkierungen, wie sie schon bei Suffix-Tries und Suffix-Bäumen verwendet wurden. Ein Knoten wird dabei mit einem Wert assoziiert, der dem längsten Pfad von der Wurzel zu dem Knoten entspricht. Etwas formaler gesehen stellt jeder Knoten ja ebenso eine Äquivalenzklasse von Knoten aus einem Trie dar, wobei mit Äquivalenz hier die Isomorphie von Teilbäumen gemeint ist. Der Repräsentant des Knotens ist dann der längste Repräsentant der Äquivalenzklasse. Daraus folgt Lemma 4.7 Ein Teilwort x von w ist das längste Mitglied der Äquivalenzklasse von x (in Zeichen val(x)) genau dann, wenn es entweder ein Präfix von w ist oder es in zwei eindeutigen linken Kontexten auftritt, das heißt sowohl ax als auch bx mit verschiedenen a und b aus Σ∗ auftreten. Nach diesen Vorüberlegungen soll nun die formale Definition eines DAWGs gegeben werden. Als Grundlage für die Definition wurde dabei die Definition nach [?] verwendet. Definition 4.8 Ein gerichteter azyklischer Wortgraph (DAWG) für ein Wort w (auch mit DAWG(w) bezeichnet) ist ein (teilweise) deterministischer endlicher Automat Dw = (Z, Σ, δ, z0 , E) mit einer Zustandsmenge Z = {[x]w |x ist ein Teilwort von w}, dem Eingabe-Alphabet Σ, einem Startzustand z0 ∈ Z mit z0 = []w , einer Übergangsfunktion δ : Z × Σ → Z mit {[x]w → a [xa]w |x und xa sind Teilworte von w und a ist ein einzelnes Zeichen} und − einer Menge von Endzuständen E. Dabei sind alle Zustände von Dw akzeptierende Zustände, das heißt E = Z. Auch wenn in der Definition von „Automat“ die Rede ist, kann man den DAWG natürlich ebenso als einen Graphen mit Anfangs- und Endknoten verstehen, in dem die Kanten zwischen den Knoten die Übergangsfunktion darstellen. Im Folgenden werden deswegen die Begriffe des „Zustands“ und des „Knotens“ synonym verwendet, da jeder Zustand des Automaten einen Knoten des Graphen darstellt und umgekehrt. Lemma 4.9 Der in Definition 4.8 definierte Automat Dw erkennt alle Teilworte von w. Beweis Da alle Knoten akzeptierende Zuständen des Automaten symbolisieren, durch jeden Knoten genau eine Äquivalenzklasse dargestellt wird und die Vereinigung aller dieser Klassen genau die Menge von Teilworten von w darstellt, folgt das Lemma unmittelbar aus der Definition. 19 Durch eine kleine Modifikation an dem Automaten schafft man es, diesen so umzuändern, dass er nicht mehr alle Teilworte eines Wortes w erkennt, sondern alle Suffixe von w. Definition 4.10 Sei Dw der oben definierte DAWG zu einem Wort w. Ändert man nun die Menge der akzeptierenden Zustände so, dass nur noch die Zustände akzeptieren, die Suffixe von w darstellen, erhält man den Automaten Sw , der ausschließlich Suffixe erkennt. Abbildung 4.1 zeigt beispielhaft den Suffix-DAWG für das Wort w = babaaabb$, der im weiteren Verlauf dieses Kapitels auch als Grundlage für die beispielhafte Herleitung mittels der Konstruktionsalgorithmen dienen soll. $ $ b Quelle a b a b a a b a a a b b b $ Senke a b a Abbildung 4.1: Suffix-DAWG für w = babaaabb$ Anhand von Definition 4.10 ist ersichtlich, dass zur Konstruktion eines Suffix-DAWGs aus einem DAWG nur die Definition der Endzustände verändert werden muss. Da das keine wesentlichen Auswirkungen auf die Eigenschaften von DAWGs hat, sollen im Folgenden die Eigenschaften von DAWGs nicht explizit auch noch für Suffix-DAWGs bewiesen werden. Es kann jedoch in allen Fällen davon ausgegangen werden, dass die Eigenschaften für Suffix-DAWGs mit denen für normale DAWGs identisch sind. Nachfolgend sollen nun einige Eigenschaften des DAWGs hinsichtlich der Menge der Knoten und Kanten aufgezählt und bewiesen werden. Hierzu ist es hilfreich, den Teilmengenbaum T (w) zu betrachten. Treffen im DAWG in einem Knoten zwei end-sets von Zeichenketten zusammen, das heißt haben zwei Teilworte aus w dieselbe Menge end-set, dann muss die Menge von end-sets der einen Zeichenkette eine Obermenge der Menge 20 der end-sets der anderen sein. Tw wird so gebildet, dass eine Kante von einem Knoten u zu einem anderen Knoten v genau dann erzeugt wird, wenn die Menge an Worten, die durch u repräsentiert wird, eine Obermenge der Menge von Worten ist, die durch v repräsentiert wird. Da die Knoten u und v die Worte repräsentieren, die dem längsten Pfad von der Wurzel zu den Knoten entsprechen, kann man auch sagen, dass eine Kante von v nach u genau dann in Tw vorhanden ist, wenn u ein Wort repräsentiert, das ein Suffix von v ist. Die einzelnen Kanten stellen somit Suffix-Links dar, wie sie bereits im vorigen Kapitel erwähnt wurden und später für die Konstruktionsalgorithmen noch näher erläutert werden sollen. Abbildung 4.2 zeigt den zu w = babaaabb gehörenden Baum Tw aus Suffix-Links. 0,1,2,3, 4,5,6,7, 8,9 1,3, 7,8 2,4 3 4 5 6 7 8 9 3,7 2,4, 5,6 5,6 Abbildung 4.2: Tw für w = babaaabb Unter Verwendung von Tw kann nun das folgende Lemma über die Eigenschaften von DAWGs hergeleitet werden. Lemma 4.11 Für |w| > 2 hat der DAWG Dw höchstens 2|w| − 1 Zustände (bzw. Knoten) und diese obere Grenze wird nur dann erreicht, wenn w = abn für beliebige a, b ∈ Σ∗ mit a 6= b ist. 21 Beweis Betrachte man zunächst den Fall, dass w = an für n > 2 gilt. In diesem Fall besteht w nur aus der Aneinanderreihung eines einzelnen Zeichens, und da a Teilwort von a2 , a2 Teilwort von a3 usw. ist, ist Tw eine einfache Kette mit n + 1 < 2n − 1 Knoten. Für alle anderen Fälle muss jetzt nur gezeigt werden, dass Tw maximal |w| Knoten mit einem Ausgangsgrad von weniger als 2 — also solche, die höchstens einen nachfolgenden Knoten (Kindknoten) haben — und damit höchstens |w| − 1 Knoten mit einem höheren Ausgangsgrad hat, sodass in der Summe die Zahl von 2|w| − 1 Knoten nicht überschritten wird. Aufgrund der Struktur von Tw taucht jeder Knoten mit einem Ausgangsgrad größer gleich 2 mindestens mit zwei unterschiedlichen Kontexten auf. Nach Lemma 4.7 können als Knoten mit einem Ausgangsgrad kleiner 2 nur noch diejenigen in Frage kommen, die die |w| + 1 Präfixe von w repräsentieren. Da das Wort w aber nicht in der Form an ist, muss zumindest einer der Präfixe, nämlich der leere, in zwei unterschiedlichen linken Kontexten auftreten. Damit kann er aber nach der Struktur von Tw kein Knoten mehr mit Ausgangsgrad kleiner gleich 2 sein. Es kommen also höchstens die |w| restlichen Präfixe und somit auch höchstens |w| Knoten als Knoten mit Ausgangsgrad kleiner gleich 2 in Frage. Man erhält somit als Summe maximal 2|w| − 1 Knoten, was den Beweis für die obere Grenze für die Zahl der Zustände abschließt. Zum Erreichen der oberen Grenze benötigt man genau |w| − 1 Knoten mit einem Ausgangsgrad größer gleich 2. Auf diese Weise erhält man mindestens |w| Blätter und kann somit die obere Grenze von 2|w| − 1 Knoten erreichen. Für die Knoten mit einem Ausgangsgrad kleiner gleich 2 kommen nur die nicht-leeren Präfixe von w in Frage. Davon gibt es genau |w| Stück. Es folgt also, dass genau die Knoten, die diese Präfixe repräsentieren, Blätter sein müssen und außerdem jeder interne Knoten genau zwei Nachfolger bzw. Kindknoten haben muss. Der einelementige Präfix von w, also derjenige, der nur aus einem einzigen Zeichen besteht, muss demnach ebenfalls ein Blattknoten sein. Aufgrund der Struktur von Tw kann er somit an keiner anderen Position sonst in w auftreten. Neben diesem Knoten kann der Wurzelknoten nur noch einen einzigen Kindknoten haben, woraus unmittelbar folgt, dass nur ein einziger weiterer Buchstabe in w vorkommen kann. Damit muss w zwangsläufig der Form abn sein. Nachdem nun die maximale Knotenzahl des DAWGs gezeigt wurde, soll im Folgenden eine Aussage über die maximale Anzahl an Kanten getroffen werden. Lemma 4.12 Für |w| ≥ 2 hat der DAWG Dw höchstens |w| − 2 Kanten mehr als Knoten. 22 Beweis Der DAWG Dw hat genau einen Startknoten (die Quelle) und einen Endknoten (die Senke). Jeder andere Knoten liegt zwangsläufig auf einem Weg vom Start- zum Endknoten und ist bekanntlich durch die Menge der Kantenmarkierungen — oder besser gesagt den längsten Repräsentanten dieser Menge —, die zu ihm führen, eindeutig bestimmt. Um das Lemma zu beweisen, bietet es sich an, sich zunächst innerhalb des DAWGs einen gerichteten Spannbaum vorzustellen, der an der Wurzel von Dw beginnt. Ein Spannbaum ist dabei so definiert, dass er einen Teilbaum darstellt, der alle Knoten des zu Grunde liegenden Graphen enthält. Man kann ihn sich wie einen Pfad durch den Graphen vorstellen, der alle Knoten abdeckt und keinen Knoten zweimal besucht. Aufgrund der Struktur des DAWGs mit seiner Quelle und Senke muss es einen solchen Spannbaum in einem DAWG geben. Nach der Definition hat ein Spannbaum genau eine Kante weniger als Knoten. Im DAWG deckt er bereits alle Knoten ab, was bedeutet, dass nur noch bewiesen werden muss, dass höchstens |w| − 1 Kanten noch nicht von ihm abgedeckt werden. Jeder Kante von Dw , die nicht vom Spannbaum abgedeckt wird, kann nun einer der |w| nicht-leeren Suffixe von w zugeordnet werden. Diese Suffixe erhält man, wenn man den Kantenmarkierungen von der Quelle des DAWGs durch den Spannbaum bis zu dem Punkt folgt, an dem die erste Kante außerhalb des Spannbaums liegt, dann der Kante selbst folgt und anschließend auf einem beliebigen, geeigneten Weg bis zur Senke läuft. Es ist klar, dass man auf diese Weise für unterschiedliche, außerhalb des Spannbaums liegende Kanten auf jeden Fall verschiedene Pfade bekommt, denn sie müssen sich zumindest an der ersten Kante, die außerhalb des Spannbaums durchlaufen wird, unterscheiden. Somit kann man durch alle diese Kanten auch unterschiedliche Suffixe assoziieren. Einer der auf diese Weise ermittelten Pfade muss zwangsläufig innerhalb des Spannbaums liegen, da dieser ja genau einen solchen Weg nach Definition abdeckt. Da es insgesamt |w| nicht-leere Suffixe in w gibt, die im Graphen durch Pfade dargestellt werden, ist die Zahl der Suffixe, die nicht von dem Spannbaum abgedeckt werden, also nach oben durch |w| − 1 beschränkt. Bei |w| Knoten erhält man also maximal |w| − 1 Kanten, die vom Spannbaum überdeckt werden, und |w| − 1 Kanten, die nicht überdeckt werden. Somit also insgesamt 2|w| − 2 Kanten, also |w| − 2 Kanten mehr als Knoten. Kombiniert man die Resultate aus den beiden Lemmata, erhält man folgenden Satz 23 Satz 4.13 Für |w| > 2 hat der gerichtete, azyklische Wortgraph für |w| höchstens 2|w| − 1 Knoten und 3|w| − 4 Kanten. Beweis Die Anzahl der Knoten folgt unmittelbar aus der Kombination der beiden Lemmata. Für die Anzahl der Kanten würde man intuitiv zunächst 3|w| − 3 als obere Grenze annehmen. Nach Lemma 4.11 gibt es jedoch nur eine einzige Situation, in der die Grenze mit 2|w| − 1 Knoten erreicht werden kann. In diesem Fall, also bei w = abn , erhält man jedoch nur 2|w| − 1 Kanten. Da die Zahl der Kanten unmittelbar von der Zahl der Knoten abhängt, kann man die Grenze für die Kantenzahl um mindestens eins verringern. Es sollte nun deutlich geworden sein, dass ein DAWG eine geeignete Möglichkeit darstellt, um feststehende Texte vorzuverarbeiten und zu speichern, denn mit seiner linearen Größe ist der Speicherplatzbedarf hierbei nicht übermäßig groß. Neben dem Speicherbedarf spielt jedoch noch eine weitere Komponente eine wichtige Rolle: Der Zeitaufwand zur Konstruktion des DAWGs. Bei kleineren Texten mag ein quadratischer oder höherer Aufwand dabei noch durchaus akzeptabel sein, doch spätestens bei umfangreichen Texten spielt der Aufwand zur Konstruktion des DAWGs eine wichtige Rolle. In den folgenden beiden Sektionen dieses Kapitels sollen nun zwei Möglichkeiten beschrieben werden, wie man aus einem Text einen DAWG konstruieren kann. Der erste Algorithmus ist dabei durch seine Einfachheit leicht zu verstehen, setzt jedoch bereits eine Vorverarbeitung an dem Text voraus. Der zweite Algorithmus arbeitet dagegen direkt auf dem Text und kommt ohne weitere Vorverarbeitungen aus, ist aber aus diesem Grund auch etwas komplexer und schwerer verständlich. Beide Algorithmen haben es gemein, dass sie den DAWG in linearer Zeit konstruieren können, wie im Folgenden noch genauer beschrieben und bewiesen werden soll. 4.2 off-line Konstruktion Zur einfachen Konstruktion eines DAWGs kann man sich seine enge Verwandschaft zu Suffix-Tries bzw. Suffix-Bäumen zu Nutze machen. Im Folgenden soll ein Algorithmus betrachtet werden, der auf Suffix-Bäumen aufbaut. Diese Methode wird auch als off-line Konstruktion bezeichnet, da der DAWG zu einem Wort w erst dann erzeugt werden kann, wenn zuvor der Suffix-Baum für dieses Wort konstruiert wurde. Grundgedanke bei der off-line Konstruktion ist es, Äquivalenzklassen von Teilbäumen zu identifizieren. Dazu existiert ein Algorithmus, der sich mit Baum-Isomorphie beschäftigt 24 und in [?] näher beschrieben wird. Dieser soll nun in Kurzform erläutert werden, für nähere Details sei auf die Literatur verwiesen. Der Algorithmus betrachtet zwei Bäume T1 und T2 und testet diese auf Isomorphie. Dazu weist er zuerst allen Blättern von T1 und T2 die Zahl 0 zu. Die Vorgänger- bzw. Elternknoten davon werden jeweils durch eine Liste von Zahlen dargestellt (S1 für T1 , S2 für T2 ), die den Zahlen der Kindknoten in nicht absteigender Reihenfolge entspricht. Die beiden Listen S1 und S2 werden anschließend verglichen. Sind sie unterschiedlich, dann sind die beiden Bäume nicht isomorph. Ansonsten wird dem Knoten ein Wert zugewiesen, welcher der Anzahl der unterschiedlichen Paare von Zahlen in S1 bzw. S2 entspricht (also beispielsweise eine 1 bei (0,0,0), eine 2 bei (0,1,1)). Diese neuen Zahlen werden nun verwendet, um erneut Listen S1 und S2 für das nächsthöhere Level in den Bäumen zu erzeugen usw. Die beiden Bäume T1 und T2 sind letztendlich isomorph, wenn ihre Wurzeln mit der selben Zahl versehen wurden. In Abbildung 4.3 wird ein Beispiel für die beiden Bäume nach dem Isomorphie-Algorithmus gezeigt. Da dort an den Wurzeln dieselben Zahlen stehen, sind T1 und T2 dort isomorph. Das folgende Lemma greift die wesentliche Eigenschaft des Algorithmus auf Lemma 4.14 Sei B ein geordneter Baum mit Wurzel, in dem die Kanten mit Buchstaben markiert sind, wobei angenommen wird, dass das in konstanter Größe geschehen ist. Dann können isomorphe Klassen von allen Teilbäumen von B in linearer Zeit berechnet werden. Der Beweis für dieses Lemma soll an dieser Stelle nicht gebracht werden, hier sei an die Originalliteratur verwiesen. Bevor auf den Konstruktionsalgorithmus und die lineare Zeit, in der er arbeitet, eingegangen werden kann, soll eine Variante von DAWGs vorgestellt werden, die für den Algorithmus verwendet wird. Definition 4.15 Ein gerichteter, azyklischer Graph, bei dem Worte als Kantenmarkierungen erlaubt sind und kein Knoten nur eine einzige eingehende Kante hat, heißt kompakter DAWG oder CDAWG über einem Wort w. Kompakte DAWGs stellen einen recht umfangreichen Themenkomplex dar, für den bereits eine Vielzahl an verschiedenen Algorithmen vorhanden ist. In dieser Arbeit soll nicht weiter auf diese spezielle Struktur eingegangen werden, die kompakten DAWGs 25 1 (1, 2) 2 (0, 1, 1) 1 (0, 0, 0) 1 (0, 0) 0 0 0 1 (0, 0) 0 0 0 0 0 1 (1, 2) 2 (0, 1, 1) 1 (0, 0, 0) 1 (0, 0) 1 (0, 0) 0 0 0 0 0 0 0 0 Abbildung 4.3: Die Bäume T1 und T2 nach dem Isomorphie-Algorithmus sollen nur als eine Art Übergangsform dienen, um aus einem Suffix-Baum letztendlich einen DAWG erzeugen zu können. Mit der Definition von kompakten DAWGs und unter Verwendung von Lemma 4.14 folgt nun Satz 4.16 Ein gerichteter azyklischer Wortgraph DAWG(w) und seine kompakte Variante können auf einem festen Alphabet in linearer Zeit konstruiert werden. Beweis Es wird ein Algorithmus vorgestellt, mit dessen Hilfe die Konstruktion des DAWGs in linearer Zeit möglich ist. Für die anschauliche Darstellung wird die BeispielZeichenkette w = babaaabb verwendet, deren zugehöriger DAWG schon früher in Abbildung 4.1 gezeigt wurde. Außerdem wird, wie bereits bei den Suffix-Tries und 26 Suffix-Bäumen, zur Konstruktion eine Endmarkierung $ ∈ / Σ an w angehängt. Voraussetzung für den Algorithmus ist außerdem ein Suffix-Baum, der wie im entsprechenden Kapitel dieser Arbeit erwähnt erzeugt wurde. Mithilfe des Algorithmus aus Lemma 4.14 werden die isomorphen Teilbäume des Suffix-Baumes berechnet und deren Wurzeln ermittelt. Damit lässt sich zunächst unmittelbar ein kompakter DAWG konstruieren, indem diese Teilbäume zusammengefasst werden. Abbildung 4.4 zeigt beispielhaft einen so erzeugten kompakten DAWG und den zu Grunde liegenden Suffix-Baum. $ b 9 a $ 8 a b$ a b 7 aabb$ 3 baaabb$ aaabb$ 1 b$ 2 bb$ abb$ 6 4 5 a b a b$ $ $ bb$ aabb$ b a abb$ b$ aaabb$ baaabb$ Abbildung 4.4: Suffix-Baum und CDAWG für w = babaaabb Der so erzeugte CDAWG muss nun noch in einen normalen DAWG umgewandelt 27 werden. Dazu ist es notwendig, die Kanten mit Markierungen von mehr als einem Zeichen zu beseitigen. Intuitiv könnte man auf die Idee kommen, einfach jede solche Kante in so viele einzelne Knoten aufzuteilen, dass die Markierungen nur noch aus jeweils einem Symbol bestehen würde. Das würde allerdings zu einer quadratischen Knotenzahl führen. Aus diesem Grund soll hier eine andere Methode verwendet werden. Man berechnet für jeden Knoten die eingehende Kante, welche die längste Markierung hat. Diese Kante bezeichnet man auch als diejenige mit dem größten Gewicht. Nun wird parallel für jeden Knoten (mit Ausnahme der Wurzel) eine Umformung ausgeführt, in der die zuvor bestimmte Kante v mit dem größten Gewicht in k Kanten aufgebrochen wird, wobei k die Länge der Kantenmarkierung z ist, mit der v beschriftet ist. Die Beschriftung der i-ten konstruierten Kante ist der i-te Buchstabe von z. Man erhält dadurch k-1 Knoten (v, 1), (v, 2), . . . , (v, k − 1), wobei Knoten (v, i) genau i Knoten von v entfernt ist. Für alle anderen Kanten e = (v1, v), also all solche, die ein kleineres Gewicht haben, wird folgende Aktion durchgeführt: Angenommen, die Markierung von e hat die Länge p und der erste Buchstabe ist a. Dann entfernt man die Kante e und erzeugt eine Kante von v1 nach (v, p − 1). Die neue Kante endet dann an einem Knoten, der genau p − 1 Kanten von v entfernt ist. Dadurch erhält man den gesuchten Graphen DAW G(w). Beispielhaft ist das in Abbildung 4.5 dargestellt, wobei die grau dargestellten Knoten im DAWG diejenigen sind, die durch das Aufspalten der Kante neu entstanden sind. Zum Abschluss des Beweises ist noch darauf hinzuweisen, dass es unbedingt erforderlich ist, dass die Aufteilungen der Kanten unabhängig voneinander sind und gleichzeitig ausgeführt werden können, um die lineare Zeit für die Konstruktion sicherzustellen. Es wurde somit gezeigt, dass ein Algorithmus existiert, der einen DAWG aus einem Suffix-Baum in O(n) Zeit konstruiert. Obwohl er in linearer Zeit arbeitet, kann der Algorithmus nicht der bestmögliche sein, da immer ein Umweg über den Suffix-Baum notwendig ist. 4.3 on-line Konstruktion Um einen DAWG Dw direkt aus einem Wort w konstruieren zu können, also ohne zuvor einen fertigen Suffix-Baum oder Suffix-Trie haben zu müssen, soll in diesem Teil des Kapitels ein on-line Konstruktionsalgorithmus eingeführt werden. Dieser Algorithmus bekommt ein Wort w übergeben und arbeitet dieses Zeichen für Zeichen von links nach recht ab. In jedem Schritt wird ein DAWG für das bisher abgearbeitete Teilwort von w erzeugt, sodass man zu Beginn zunächst einen DAWG für das leere Wort hat, dann 28 a b a b $ a b$ $ b a a b b $ bb$ $ abb$ b a aabb$ b b$ aaabb$ a baaabb$ a a a b a b b $ Abbildung 4.5: Transformation CDAWG → DAWG für w = babaaabb 29 einen für den ersten Buchstaben von w, dann einen für den zweiten und so weiter. Der jeweilige gerichtete azyklische Graph ist dabei in jedem Schritt bereits vollständig. Man kann also aus den Schritten der Konstruktion unmittelbar die DAWGs für alle Teilworte von w ablesen. Zu beachten ist in diesem Zusammenhang noch, dass die Konstruktion des DAWGs Dw , also des DAWGs, der alle Teilworte von w erkennt, dieselbe ist wie die des DAWGs Sw , der nur die Suffixe von w erkennt. Erlaubt man interne Knoten als Endzustände, bekommt man Dw , während man durch Definition der Senke als einzigen Endknoten nur noch Suffixe als akzeptierende Worte erlaubt und somit Sw bekommt. Im Folgenden ist aus diesem Grund, wenn von Dw die Rede ist, auch stets Sw gemeint, wenn nicht explizit anders angegeben. Notwendig für das Erzeugen eines Konstuktionsalgorithmus ist es, ausgehend von einem DAW G(u), wobei u ein Präfix von w ist, zunächst festzustellen, welche Änderungen sich ergeben, wenn man das nächste Zeichen a aus w hinzunimmt, man also DAW G(ua) konstruiert. Hierzu soll zunächst eine Definition gegeben werden, die für spätere Beweise benötigt wird. Definition 4.17 Sei w = w1 yw2 ein Wort mit w1 , w2 , y ∈ Σ∗ , y 6= . Dann heißt y in w das erste Auftreten von y in einem neuen linken Kontext, wenn y wenigstens zweimal in w1 y auftritt und vor jedem Auftreten außer dem letzten von y in w1 y ein gleiches Symbol a ∈ Σ auftritt. taucht gemäß Festlegung nie in einem neuen linken Kontext auf. Beispiel 4.18 Sei w = abcbc. Das zweite Auftreten von bc stellt das erste Auftreten von bc in einem neuen linken Kontext dar. Sei w = bcbc. In diesem Fall gilt die Aussage nicht, denn vor dem Auftreten des ersten bc in w müsste ein weiterer Buchstabe stehen. Ausgehend von dieser Definition können nun einige Eigenschaften gezeigt werden, die der DAWG für das Teilwort ua von w mit einem Teilwort u ∈ Σ∗ und einem einzelnen Zeichen a ∈ Σ+ in Abhängigkeit vom zuvor erzeugten DAW G(u) hat. Lemma 4.19 1. ua stellt in DAW G(ua) eine Äquivalenzklasse dar, die alle Teilworte von ua enthält, die kein Teilwort von u sind. Das heißt, dass ein neuer, in DAW G(uaa) hinzugefügter Knoten bzw. Zustand genau die Worte repräsentiert, die bisher noch nicht in DAW G(u) repräsentiert wurden. Diese Eigenschaft wird im späteren Algorithmus in der Update-Phase ausgenutzt. 30 2. Für jedes Teilwort x von u gilt: Wenn x im DAWG von u bereits eine Äquivalenzklasse repräsentiert, dann repräsentiert er eine solche auch im DAWG für ua. Die Mitglieder der beiden Äquivalenzklassen sind dieselben, es sei denn, x ≡w tail(ua) und tail(ua) erscheint erstmals in einem neuen linken Kontext. In diesem Fall muss die Äquivalenzklasse von x in zwei Klassen aufgeteilt werden, wobei alle Worte, die länger als tail(ua) sind, in der bisherigen Äquivalenzklasse von x bleiben, während alle anderen einer neuen Klasse [tail(ua)]ua zugeordnet werden, der durch tail(ua) repräsentiert wird. Am DAWG betrachtet bedeutet das folgendes: Repräsentiert ein Knoten x in DAW G(u) ein Wort, dann repräsentiert er dasselbe zunächst auch in DAW G(ua). Repräsentiert er aber genau das längste Suffix von ua und tritt noch dazu erstmals in einem neuen linken Kontext auf, dann muss ein neuer Knoten erstellt und die Kanten so verlegt bzw. hinzugefügt werden, dass x anschließend nur noch längere Wörter als tail(ua) repräsentiert und der neue Knoten all solche, die genauso lang oder kürzer sind. Im Algorithmus wird das später durch eine Split-Phase umgesetzt. 3. Außer den beiden in Punkt 1 und 2 erwähnten Äquivalenzklassen gibt es keine weiteren in ≡wa . Es sind somit bereits alle Fälle, die beim Übergang von DAW G(u) nach DAW G(ua) auftreten können, abgedeckt. Beweis 1. ua repräsentiert immer eine Äquivalenzklasse, da es ein Präfix von sich selbst ist. In dieser Klasse sind alle diese Teilworte, die innerhalb von w genau an der letzten Position von ua enden. Das heißt, es handelt sich bei diesen Worten genau um die neuen Teilworte von ua, die noch nicht in u auftreten. 2. Jedes Wort, das in u entweder als Präfix auftritt oder in zwei unterschiedlichen linken Kontexten vorkommt, muss das zwangsläufig auch in ua tun. Wird also durch ein x eine Menge solcher Worte in DAW G(u) repräsentiert, dann muss dieselbe Menge auch in DAW G(ua) dadurch repräsentiert werden. Der Fall, dass [x]u 6= [x]ua ist, also die von x repräsentierten Äquivalenzklassen in DAW G(u) und DAW G(ua) unterschiedlich sind, kann nur dann auftreten, wenn es ein y ∈ [x]u gibt, das nicht in [x]ua auftritt. Das Gegenteil davon, also den Fall, dass ein y ∈ [x]ua existiert, das nicht in [x]u ist, kann ausgeschlossen werden, da u eine Teilmenge von ua ist. Sei y nun das längste Wort in der Äquivalenzklasse [x]w , das nicht in [x]ua auftritt, dann ist nach Lemma 4.5 x = tby mit t ∈ Σ∗ , b ∈ Σ. Da u in DAW G(u) in der Äquivalenzklasse von x enthalten ist, kann y in u somit ausschließlich nach tb auftreten. Da außerdem u nicht in der Äquivalenzklasse von x in DAW G(ua) ist, muss y ein Suffix von ua sein, dem nicht tb vorangestellt ist. 31 Es bestünde nun noch die Möglichkeit, dass by ein Suffix von ua ist. Dann wäre jedoch by ≡u x und by ≡ua y, was einen Widerspruch zur Maximalität von y darstellt. Es bleibt also nur der Fall, dass cy ein Suffix von ua mit einem beliebigen Symbol c 6= b ist. Da cy nicht in u auftreten kann, folgt daraus, dass y = tail(ua) ist und dass tail(ua) zum ersten Mal in einem neuen linken Kontext auftritt. Außerdem folgt daraus, dass das Wort y und alle seine Suffixe in der Äquivalenzklasse von x in DAW G(u) als Suffixe von ua auftreten und es keine längeren Worte als y geben kann. Deshalb wird [x]w in zwei Klassen geteilt, eine repräsentiert durch y = tail(wa) für y selbst und kürzere Worte und die andere durch x für die übrigen Worte. 3. Durch Teil 1 und 2 wurden bereits alle Teilworte von wa abgedeckt. Deshalb kann es keine anderen Äquivalenzklassen geben. Um den Übergang von DAW G(u) nach DAW G(ua) zur realisieren, verwendet man zwei besondere Konstruktionsdetails, die den Algorithmus vereinfachen. Hierbei handelt es im Wesentlichem um die Einführung verschiedener Kantenarten sowie Suffix-Links. Normalerweise werden in Graphen nur eine Art Kanten verwendet. Für die DAWGKonstruktion ist es jedoch sinnvoll, diese etwas näher zu differenzieren. Man führt für den Algorithmus zwei Arten von Kanten ein: primäre Kanten sind solche, die während der gesamten Konstruktion des DAWGs nicht geändert werden. Das bedeutet, dass eine einmal eingefügte primäre Kante in jedem Schritt des Konstruktionsalgorithmus erhalten bleibt. Sekundäre Kanten dagegen sind nur übergangsweise vorhanden. Sie können in einem Konstruktionsschritt erzeugt, dann aber im nächsten Schritt wieder verändert werden. Diese Differenzierung der Kanten hat während der Konstruktion einige wesentliche Vorteile, auf die später in diesem Kapitel näher eingegangen wird. Suffix-Links stellen eine Art Zeiger dar, die von einem Knoten im DAWG auf einen anderen verweisen. Jeder Knoten im DAWG mit Ausnahme der Wurzel verweist dazu auf seinen Vaterknoten im Teilmengenbaum T (w), der bereits zuvor in dieser Arbeit erwähnt wurde. Folgt man einem Suffix-Link von einem Knoten v aus, gelangt man zu einem Knoten, der einen Suffix des Wortes darstellt, den v repräsentiert. Durch die Einführung von Suffix-Links ist es nun also möglich, von jedem Knoten x im Baum ausgehend zurück zur Wurzel zu gehen, indem man den Suffix-Links in einer Art Kette folgt. Eine solche Kette wird auch Suffix-Kette genannt und im Folgenden auch als SC(x) bezeichnet. Die Länge dieser Kette ist dann entsprechend |SC(x)|. Es gilt dabei folgendes: 32 Lemma 4.20 1. Für irgendein Wort x, das im DAWG zu w durch einen Knoten repräsentiert wird, teilt SC(x) alle Suffixe von x in |SC(x)| Klassen ein, das heißt durch Folgen der Suffix-Links vom Knoten, der x repräsentiert, bis zur Wurzel kann man alle Knoten erreichen, die Suffixe von x repräsentieren. Ist x = w, dann können alle Suffixe von w dadurch ermittelt werden, dann man der Kette aus Suffix-Links von der Senke bis zur Quelle folgt. 2. Ist w nicht das leere Wort, dann zeigt der Suffix-Link der Senke von Dw auf [tail(w)]w . 3. Der erste Knoten auf dem Weg über die Suffix-Links von der Senke von Dw bis zur Quelle, der einen a-Übergang hat, das heißt eine mit a beschriftete, ausgehende Kante, muss einen a-Übergang zu [tail(wa)]w haben. Wird kein aÜbergang gefunden, dann bedeutet das, dass a nur einmal in wa auftritt und deswegen tail(wa) = ist. Durch die Einführung der Suffix-Links und Kantenarten kann man bei der Konstruktion des DAWGs in jedem Schritt genau feststellen, wo Knoten neu erstellt werden und Kanten aufgeteilt werden müssen. Suffix-Links helfen in diesem Fall dabei, die Knoten zu finden, an denen geteilt werden muss, primäre und sekundäre Kanten geben Informationen darüber, welche Knoten und Kanten aufgeteilt werden können bzw. müssen und welche nicht. Im Folgenden sollen nun die einzelnen Schritte des Algorithmus erläutert werden, der den DAWG konstruiert. Im Anschluss daran wird die Konstruktion anhand eines detaillierten Beispieles näher erläutert und die lineare Zeit des Algorithmus bewiesen. Der Konstruktions-Algorithmus ist in drei Methoden aufgeteilt: Die Hauptmethode, eine Methode Update und eine Methode Split. Der Ablauf ist dabei wie folgt: Der Algorithmus bekommt ein Wort w übergeben. In der Hauptmethode wird zu allererst ein Knoten erzeugt, der die Quelle des DAWGs repräsentieren soll. Anschließend wird w von links nach rechts zeichenweise durchlaufen. Dabei ruft die Hauptmethode für jeden aktuellen Buchstaben aus w die Methode Update auf. Als zusätzlichen Parameter bekommt Update den Knoten übergeben, der die aktuelle Senke des DAWGs darstellt. Dieser ist zu Beginn des Algorithmus die Quelle, in allen folgenden Schritten der Knoten, der von Update zurückgegeben wird. Ist das Wort w einmal komplett durchlaufen, gibt der Algorithmus als Ergebnis den Quell-Knoten des erzeugten DAWGs zurück und wird beendet. Die eigentliche Konstruktion des DAWGs für ua aus dem Graphen zu u erfolgt in der Methode Update. Darin werden nacheinander folgende Schritte ausgeführt: 1. Es wird ein neuer Knoten s im DAWG erzeugt. Dieser stellt für den aktuellen Schritt die neue Senke von DAW G(ua) dar und repräsentiert gemäß Lemma 4.19 33 (1) eine Äquivalenzklasse des aktuellen DAWGs. s wird mit einer primären Kante an den Knoten gehängt, der zuvor die Senke von DAW G(u) gewesen ist. Die Markierung der Kante ist dabei a. In jedem Schritt wird quasi eine Kette aus Knoten und primären Kanten gebildet, die das komplette Wort im jeweiligen Schritt repräsentiert. 2. Es wird ein Knoten v definiert, der den Wert des Knotens bekommt, den man über den Suffix-Link der alten Senke erreicht, oder den Wert null, falls es keinen solchen Suffix-Link gibt. Außerdem wird ein Zeiger link definiert, der später auf den Knoten zeigen soll, zu dem in Schritt 3 ein Suffix-Link erzeugt werden soll. Zunächst sei link gleich null. Ist v = null, dann ist v nach Lemma 4.19 (3) die Quelle des DAWGs. In diesem Fall wird link auf den Quellknoten des DAWGs gesetzt und der Algorithmus mit Schritt 3 fortgesetzt. Ist v 6= null, dann wird überprüft, ob es von v aus eine Kante gibt, die mit dem aktuellen Buchstaben a aus dem Wort w beschriftet ist. Hier sind drei Fälle zu unterscheiden: a) Hat der Knoten v keine ausgehende Kante, die mit a beschriftet ist, dann muss eine sekundäre Kante von v zur neuen Senke s des DAWGs erzeugt werden, die mit a beschriftet wird. In diesem Fall wiederholt man Schritt 2, wobei v nun den Wert bekommt, den man durch Folgen des Suffix-Links von v aus bekommt. b) Hat v eine primäre ausgehende Kante, die mit a beschriftet ist, dann müssen nach Lemma 2.19 (2) keine weiteren Veränderungen im DAWG vorgenommen werden. link wird auf den Knoten gesetzt, den man durch Folgen der mit a markierten Kante erreicht und der Algorithmus wird mit Schritt 3 fortgesetzt. c) Hat v eine sekundäre ausgehende Kante, die mit a beschriftet ist, dann muss diese Kante aufgebrochen werden. Dazu wird die Methode Split aufgerufen, die alle notwendigen Änderungen nach Lemma 4.19 (2) durchführt. Als Parameter bekommt Split den Knoten v sowie den Knoten übergeben, zu dem die von v ausgehende, mit a beschriftete Kante führt. Der Rückgabewert ist der neuen Wert für link. Anschließend wird mit Schritt 3 fortgefahren. 3. Es wird ein Suffix-Link von s zu link erzeugt und s wird zurückgegeben. Der neue DAWG für das Teilwort ua ist nun vollständig erzeugt worden. Die Methode Split führt all die Modifikationen durch, die beim Teilen einer Kante notwendig sind. Split bekommt als Parameter zwei Werte, den Knoten v sowie dessen Kindknoten y, übergeben. Zwischen diesen beiden Knoten existiert die sekundäre Kante, die aufgebrochen werden soll. Dazu werden folgende Schritte ausgeführt: 1. Es wird ein neuer Knoten y 0 erzeugt. Dieser soll später einen Klon von y darstellen. 34 2. Die sekundäre Kante zwischen v und y wird entfernt und stattdessen wird eine gleich markierte, jetzt jedoch primäre, Kante zwischen v und y 0 erzeugt. 3. Alle ausgehenden primären und sekundären Kanten von y werden geklont. Das bedeutet, dass all diese Kanten nun auch ausgehend vom Knoten y 0 erzeugt werden, wobei sie zu denselben Knoten führen wie von y aus und dieselben Markierungen bekommen. Die einzige Änderung besteht darin, dass alle Kanten, die von y 0 ausgehen, sekundäre Kanten sind, unabhängig davon, ob sie vorher sekundär gewesen sind oder nicht. 4. Der Suffix-Link vom Knoten y 0 wird auf den Knoten gesetzt, auf den der SuffixLink von y zeigt. Anschließend wird der Suffix-Link von y auf y 0 gesetzt. 5. Für den Fall, dass v nicht die Quelle ist, kann es sein, dass es noch sekundäre Kanten gibt, deren Positionen verändert werden müssen. Dazu wird dem Suffix-Link von v zum Knoten z gefolgt und überprüft, ob es von dem Knoten aus eine sekundäre Kante zu y gibt. Ist das der Fall, wird diese Kante gelöscht und stattdessen eine sekundäre Kante mit derselben Markierung zum Knoten y 0 erzeugt. Außerdem wird die Suffix-Kette weiter nach oben gelaufen und die Überprüfung fortgesetzt. Ist das nicht der Fall, gibt es also keine Kante, die umgelegt werden muss, oder wird beim Folgen der Suffix-Links die Wurzel erreicht, fährt man in Schritt 6 fort. 6. Der Knoten y 0 wird zurückgegeben. Da die Rückgabe an die Methode Update geschieht, entspricht dieser Wert dem neuen Wert von link. Führt man den Algorithmus so durch, wird man feststellen, dass man am Schluss im DAWG sowohl primäre als auch sekundäre Kanten vorliegen hat. Da die Unterscheidung zwischen diesen beiden Kantenarten nur für den Algorithmus, nicht aber für den DAWG an sich entscheidend ist, braucht man sich damit nicht weiter beschäftigen und kann davon ausgehen, dass beide Kantenarten im endgültigen DAWG gleichbedeutend sind. Das folgende Beispiel 4.21 zeigt schrittweise die Konstruktion eines DAWGs für das Wort w = babaaabb$. Hierbei ist darauf hinzuweisen, dass das $-Zeichen als Endmarkierung für die korrekte Konstruktion des DAWGs hier zwingend angehängt werden muss, da ansonsten mehrere Suffixe des Wortes w = babaaabb nicht im DAWG dargestellt werden. Der DAWG für das Wort w mit Endmarkierung wird dabei, wie im Beispiel erkennbar, völlig analog zu den anderen Konstruktionsschritten aus dem DAWG für das Wort w ohne Endmarkierung hergeleitet. Beispiel 4.21 Sei w = babaaabb$. Der Konstruktionsalgorithmus erzeugt zunächst einen Knoten 1 für die Quelle. Anschließend wird ein neuer Knoten 2 erzeugt, der mit einer primären Kante (im Folgenden als Pfeil mit dicker Linie dargestellt) mit der Quelle verbunden wird. Die Kantenmarkierung ist dabei der erste Buchstabe von w, also das 2 auf die b. Außerdem wird der Suffix-Link (dargestellt als gestrichelter Pfeil) von Quelle gesetzt und der DAWG für b somit vervollständigt. 35 2 zu einem neuen Knoten Für DAW G(ba) wird eine primäre Kante a vom Knoten 3 erzeugt. Gemäß Schritt 2a) der Update-Methode muss anschließend noch eine se 1 nach 3 erzeugt werden. Der Suffix-Link von 3 zeigt auf die kundäre Kante a von 1 (siehe Abbildung 4.6). Quelle a 1 1 b 2 1 2 b 3 a Abbildung 4.6: DAWGs zu , b und ba Die DAWGs für bab und baba werden entsprechend erzeugt, die Suffix-Links werden 1 und 3 gesetzt dabei allerdings in Schritt 2b) der Update-Methode auf die Knoten (siehe Abbildung 4.7). a 1 b a 2 a 3 b 4 1 b 2 a 3 b 4 a 5 Abbildung 4.7: DAWGs zu bab und baba Für die Konstruktion von DAW G(babaa) muss nun der Buchstabe a in den DAWG eingefügt werden. Dazu wird gemäß dem Algorithmus zunächst nach Schritt 1 der 6 eingefügt sowie eine primäre Kante a. Nach Methode Update ein neuer Knoten 5 zum Knoten 3 gefolgt. Da es dort keine Schritt 2 wird nun dem Suffix-Link von mit a markierte, ausgehende Kante gibt, wird nach 2a) eine sekundäre Kante zu 6 erzeugt und anschließend dem Suffix-Link von 3 nach 1 gefolgt. 1 hat eine ausgehende, sekundäre Kante a, also muss nach 2c) eine Split-Operation durchgeführt 1 und . 3 werden, aufgerufen mit den Knoten 7 erzeugt. In Schritt 2 wird die a-Kante von In Schritt 1 von Split wird der Knoten 1 nach 3 entfernt und dafür eine primäre Kante a von 1 nach 7 erzeugt. In Schritt 3 nach 6 eine zusätzliche sekundäre von 7 3 wird für die sekundäre a-Kante von 6 erzeugt, für die primäre b-Kante von 3 nach 4 eine zusätzliche sekundäre nach 7 nach . 4 von 7 auf 1 und von 3 auf , 7 wobei der vorher Schritt 4 setzt die Suffix-Links von 3 nach 1 durch diesen ersetzt wird. vorhandene Suffix-Link von 1 entspricht dem v aus Schritt 5 entfällt, weil die Quelle bereits erreicht ist (Knoten dem Algorithmus). 36 7 zurückgegeben, sodass in der Update-Methode ein In Schritt 6 wird nun der Knoten 6 auf 7 erzeugt wird. Damit ist die Konstruktion von DAW G(babaa) Suffix-Link von abgeschlossen (siehe Abbildung 4.8). a a 1 2 b 3 a 4 b 5 a 6 a a 1 2 b 3 a b 4 a 5 a 6 b a a 7 Abbildung 4.8: DAWG zu babaa vor und nach dem Split Für DAW G(babaaa) und DAW G(babaaab) müssen jeweils wieder Split-Operationen ausgeführt werden, die aber analog zu denen für DAW G(babaa) ablaufen. Der DAWG zu babaaabb bringt noch einmal zwei neue sekundäre Kanten in den Graphen, deren Konstruktion jedoch ebenfalls analog abläuft. Abbildung 4.8 und 4.9 zeigen diese letzten Schritte des Konstruktionsalgorithmus. Die Konstruktion des DAWGs für das komplette Wort babaaabb$ verdeutlicht, warum zwingender Weise das $-Zeichen an w gehängt werden muss. In DAW G(babaaabb) wird weder das leere Suffix noch das Suffix b repräsentiert. Dagegen hilft es, den 1 und 2 zur Senke DAWG für babaaabb$ zu erzeugen, da dort die Kanten von eingefügt werden, die schließlich dafür sorgen, dass auch alle Suffixe von babaaabb im DAWG dargestellt werden. Satz 4.22 Der obige Algorithmus zur Konstruktion von DAW G(w) arbeitet in linearer Zeit. Beweis Sei w ein Wort der Länge n und sinki die Senke von DAW G(w[1..i]). Man betrachte den Pfad, der durch die Knoten v1 = tail[sink], v2 = tail[v1 ], . . . , v = tail[wk ] gegeben ist. Dieser Pfad ist der Arbeitspfad, der in einer Iteration des Algorithmus betrachtet wird. Sei die Länge des Pfades im Folgenden k1 . 37 a 1 2 b 3 4 a 5 b 6 a 8 a a b a a 7 a 1 2 b 3 a 4 b 5 a 6 a 8 a b a a 7 a 9 a 1 2 b 3 a b 4 5 a 6 a 8 a 10 b b a 7 a a b 9 a 1 b 2 3 a b 4 a 5 a 6 a 8 b a a 11 a b 7 a b 9 Abbildung 4.9: DAWGs babaaa und babaaab 38 10 b a 1 b 2 3 a 4 b a 5 6 a 8 a b 10 b 12 a a a 11 b b 7 a 9 b $ $ b a 1 b 2 3 a b 4 a 5 a 6 8 a b 10 b a a a 11 b b 7 a 9 b Abbildung 4.10: DAWGs babaaabb und babaaabb$ 39 12 13 $ Neben der Länge des Arbeitspfades spielt die Anzahl der Kanten, die in Schritt 2 der Methode Update sowie in Split geändert wurden, zur Einschätzung der Komplexität eine wichtige Rolle. Sei die Zahl der so geänderten Kanten k2 . Die Komplexität Ki einer Iteration ist also durch die Summe aus der Arbeitspfadlänge und der Zahl der geänderten Kanten gegeben, also Ki = k1 + k2 . Sei tiefe(v) die Anzahl der Schritte, die man im vollständigen DAWG zu w braucht, um mittels Suffix-Links vom Knoten v zur Quelle zu kommen. Damit gilt folgende Ungleichung: tief e(sinki+1 ) ≤ tief e(sinki ) − Ki + 2, umgeformt also Ki ≤ tief e(sinki ) − tief e(sinki+1 ) + 2 Da also in jedem Schritt des Algorithmus maximal lineare Zeit benötigt wird, muss auch in der Summe aller Iterationen eine lineare Zeitkomplexität herauskommen. Somit ist der Satz bewiesen. 4.4 Beziehung zwischen Suffix-Bäumen und DAWGs Suffix-Bäume und DAWGs stehen in einer engen Beziehung zueinander. Wie bereits aus früheren Abschnitten dieser Arbeit bekannt ist, lassen sich etwa beide aus demselben Suffix-Trie herleiten. Das ist jedoch nicht die einzige Gemeinsamkeit zwischen den beiden Strukturen. Im Folgenden sollen nun einige interessante Beziehungen näher erläutert werden. Um sich über diese Beziehungen klar zu werden, ist eine entgegengesetzte Sichtweise anzuwenden wie bisher. Definition 4.23 Sei w ein Wort über einem Alphabet Σ. Dann bezeichnet wR das Wort, das gebildet wird, indem man die Buchstaben von w in umgekehrter Reihenfolge aufschreibt. Beispiel 4.24 Sei w = baabb, dann ist wR = bbaab. Betrachtet man nun ein Wort w, dann gilt nach Definition 4.3, dass zwei Teilworte x und y von w isomorph genau dann sind, wenn ihre end-sets gleich sind. Das bedeutet auch, dass in diesem Fall das eine Teilwort ein Suffix des anderen sein muss. Bildet man aus w nun wR , dann werden aus Suffixen Präfixe und aus der Menge der End-Positionen end-set wird die Menge der Startpositionen start-pos. start-pos(x) gibt also die Menge der Positionen an, an denen ein Teilwort x in w beginnt. 40 Das folgende Lemma nennt einige Eigenschaften, die zum Erkennen der Gemeinsamkeiten zwischen Suffix-Bäumen und DAWGs essentiell wichtig sind. Lemma 4.25 Angenommen, w habe ein eindeutiges linkes Startzeichen. Dann gilt: 1. end-set(x) = end-set(y) in w 2. start-pos(xR ) = start-pos(y R ) in wR 3. xR und y R sind in derselben Kette im Suffix-Trie zu wR enthalten Hierzu ist anzumerken, dass es nicht zwingend notwendig für die Beziehung zwischen DAWGs und Suffix-Bäumen ist, dass man ein eindeutiges linkes Startsymbol wählt. Auch ohne diese Voraussetzung lässt sich im Wesentlichen dasselbe zeigen, die Wahl des eindeutigen Startsymbols soll hier nur vereinfachend verwendet werden. Die Punkte 1 und 2 des Lemmas wären auch dann, wenn das linke Startzeichen von w nicht eindeutig ist, immer noch in jedem Fall erfüllt. Lediglich Punkt 3 könnte unter Umständen nicht mehr gelten. Für w = abba würde sich etwa ergeben, dass zwei Teilworte a und ab in derselben Kette im Suffix-Trie enthalten wären, aber start-pos(a) = {1, 4} und start-pos(ab) = {1} gilt. Dieser Fall ist durch die Einführung des linken Startsymbols auszuschließen. Betrachtet man einen Suffix-Baum für das Wort wR , dann gehört jeder Knoten daraus zu genau einem Knoten im DAWG für w und umgekehrt. Das liegt daran, dass im SuffixBaum für wR jeder Knoten genau der längste Repräsentant der Äquivalenzklassen von Teilworten aus w ist und diese längsten Repräsentanten im DAWG für das Wort w durch Knoten repräsentiert werden. Um aus einem Suffix-Baum den zugehörigen DAWG zu gewinnen, ist es zunächst notwendig, dass man einen Suffix-Baum hat, in dem jedes Suffix des zugrunde liegenden Wortes durch einen Knoten repräsentiert wird. Ein solcher Baum wird zum Beispiel mit Hilfe des Ukkonen-Algorithmus konstruiert. Außerdem benötigt man Links, die den Suffix-Links ähneln, nur in die entgegengesetzte Richtung zeigen. Diese Links heißen ErweiterungsLinks und seien im Folgenden mit ext[α, u] bezeichnet, wobei der Wert von ext[α, u] der Knoten v ist, dessen Wert das kürzeste Wort mit dem Präfix αx ist, wobei x der Wert des Knotens u ist. Gibt es keinen solchen Knoten, dann ist ext[α, u] = nil. Satz 4.26 1. DAWG(w) ist der Graph, der aus den Erweiterungs-Links des Suffix-Baums für wR gebildet wird. 2. Primäre Kanten im DAWG zu w sind umgekehrte Suffix-Links aus dem SuffixBaum für wR . 41 Beweis Die Knoten des DAWGs zu w korrespondieren mit den Knoten aus dem Suffix-Baum zu wR . Im DAWG gibt es eine Kante α von einem Knoten u, der den Wert x darstellt, zu einem Knoten v, der den Wert y darstellt, genau dann wenn y der längste Repräsentant der Klasse von Teilworten ist, die das Wort xα enthält. In diesem Fall sind die beiden Knoten also isomorph und end-set(y) = end-set(xα). Betrachtet man nun statt w das umgedrehte Wort wR , dann bedeutet das, dass αxR das längste Wort y R ist, für das start-pos(y R ) = start-pos(αxR ) im Text wR gilt. Das bedeutet, dass y R = ext[α, xR ] ist und es einen Suffix-Link mit der Beschriftung α von xR nach y R gibt. Daraus lässt sich schließen, dass durch die Menge der Suffix-Links genau die primären Kanten von DAWG(w) gegeben sind. Hat man also einen Suffix-Baum gegeben, kann man daraus recht einfach einen DAWG für das umgedrehte Wort gewinnen. Voraussetzung dafür, dass das in linearer Zeit möglich ist, ist das Vorhandensein einer Tabelle oder einer anderen Datenstruktur zum Speichern der Suffix-Links und die Berechnung der Erweiterungs-Links in linearer Zeit. Satz 4.27 Hat man einen Suffix-Baum T für ein Wort w gegeben und zusätzlich eine Tabelle, in der die Suffix-Links von T gespeichert sind, dann lassen sich die ErweiterungsLinks in linearer Zeit berechnen. Beweis Man betrachte umgedrehte Erweiterungs-Links. Gilt suf [a·α] = α, dann ist ext[α, a] = a · α, wobei die Knoten durch Zeichenketten identifiziert werden. Nun bearbeitet man den Baum von unten nach oben folgendermaßen: Ist ext[u, α] = nil und ext[w, α] = nil für einen Kindknoten w von u, dann ist ext[u, α] = ext[w, α]. Man erkennt, dass dieser Vorgang in linearer Zeit über einem konstant großen Alphabet möglich ist. Es wurde nun also gezeigt, dass man aus Suffix-Bäumen relativ einfach DAWGs erzeugen kann. Ebenso ist es möglich, aus DAWGs Suffix-Bäume herzuleiten. Dazu muss man für jeden Suffix-Link von y nach x des DAWGs eine Kante xR → y R einfügen, die mit y R xR beschriftet ist, wobei von xR der Präfix entfernt wird. Dann gilt Satz 4.28 Der Baum mit den umgedrehten Suffix-Links des DAWGs für das Wort w ist der Suffix-Baum für das Wort wR . 42 Beweis Der Beweis erfolgt dabei auf dieselbe Weise wie der Beweis von Satz 4.26. 4.5 Zeichenkettensuche mit DAWGs Es gibt viele denkbare Verwendungszwecke für DAWGs in der Zeichenkettensuche. Aufgrund der engen Verwandtschaft zu den Suffix-Bäumen sind viele dieser Verwendungsmöglichkeiten bei DAWGs und Suffix-Bäumen identisch. So können zum Beispiel das erste und letzte Auftreten einer Zeichenkette w in text oder die Anzahl der Vorkommen von w in text bei festem Alphabet in linearer Zeit mithilfe beider Datenstrukturen bestimmt sowie die Anzahl der Teilworte in einem Wort text ebenfalls in linearer Zeit berechnet werden. Ebenso ist es möglich, längste gemeinsame Teilworte zweier Wörter x und y bei einem festen Alphabet in linearer Zeit zu bestimmen. Dazu sollen nun kurz zwei Algorithmen vorgestellt werden, die dafür DAWGs nutzen. Der erste Algorithmus, im Folgenden auch als forward-dawg-matching-Algorithmus oder FDM bezeichnet, wurde von Crochemore in [?] vorgestellt und stellt eine Variante des Morris-Pratt-Algorithmus zur Zeichenkettensuche dar. FDM bekommt zwei Wörter w, text ∈ Σ∗ übergeben, wobei w das kürzere der beiden Wörter ist, und berechnet für jede Position in text die längsten Teilworte von w, die dort enden. Für w wird DAW G(w) erstellt, während text vom Algorithmus von links nach rechts verarbeitet wird. In jedem Schritt sei p ∈ Σ∗ das Präfix von text, das schon verarbeitet wurde. Das längste Suffix s ∈ Σ∗ davon, das in w vorkommt, ist durch vorige Verarbeitungsschritte bekannt. Um nun den Wert zu berechnen, der zu dem nächsten Präfix pa des Textes gehört, wobei a ∈ Σ das nächste Zeichen in text ist, kann man den DAWG für das Wort w benutzen. Ist sa ein Teilwort von w, dann muss im DAWG nur einer Kante gefolgt werden, um den nächsten Knoten zu bekommen, der dieses Wort repräsentiert. Ansonsten folgt man im DAWG den Suffix-Links, um zu dem entsprechenden Knoten zu kommen, was in etwa der Fehlerfunktion des Morris-Pratt-Algorithmus entspricht. FDM muss also, um korrekt arbeiten zu können, in jedem Schritt einen Zustand bzw. Knoten von DAW G(w) speichern. Da es aber möglich ist, Knoten auf mehreren Pfaden zu erreichen, muss sichergestellt werden, dass auch der korrekte Pfad zu dem Knoten genommen wird, da ansonsten ein anderes längstes Teilwort erkannt wird. Dazu wird zusätzlich zu dem aktuellen Zustand noch die Länge des längsten Teilwortes berechnet, das an der aktuellen Position in text endet. Folgt man nun einem Suffix-Link, wird diese Länge einfach auf die Länge des längsten Pfades gesetzt, mit dem man den Knoten, zu dem der Suffix-Link führt, erreichen kann. 43 Der Algorithmus arbeitet so bereits korrekt, kann aber noch weiter verbessert werden, indem man ähnlich wie bei der Verbesserung des Morris-Pratt-Algoritmus zum KnuthMorris-Pratt-Algorithmus durch Vorverarbeitung an w einige überflüssige Suchschritte verhindert und so das Delay, also die Zeit, die für einen gegebenen Buchstaben des Textes aufgebracht wird, verringert. Abschließend kann festgehalten werden, dass FDM die längsten gemeinsamen Teilworte von w und text in linearer Zeit bei konstantem Alphabet berechnen kann, für den Beweis hierzu sei auf die Erläuterungen weiter oben sowie auf die Originalliteratur verwiesen. Ein zweiter Algorithmus ist der backward-dawg-matching-Algorithmus oder BDM, der von Crochemore und anderen in [?] vorgestellt wird. Er stellt eine Variante des BoyerMoore-Algorithmus dar. BDM sucht nach einem Teilwort x aus einer Zeichenkette w, das an der aktuellen Position in text endet, wobei w und text wie oben definiert sind und x ∈ Σ∗ ist. BDM verwendet dabei eine Art Suchfenster über dem Text, das mithilfe einer shiftFunktion verschoben werden kann. Dieses Suchfenster wird von rechts nach links durchsucht. Das aktuell betrachtete Teilwort x wird im BDM-Algorithmus mithilfe des Knotens im DAWG für das umgedrehte Wort wR identifiziert, der zu xR gehört. Das umgedrehte Wort wird wegen der Suchrichtung von rechts nach links verwendet. In jedem Schritt des Algorithmus wird getestet, ob für den aktuellen Buchstaben a des Textes ax zu einem Teilwort in w gehört. Dazu muss in DAW G(wR ) nach einer mit a beschrifteten Kante gesucht werden, die vom aktuellen Knoten ausgeht. Hat man eine solche Kante gefunden, gehört ax zu einem Teilwort in w, ansonsten wird das Suchfenster mithilfe der shift-Funktion verschoben. Die shift-Funktion kann bereits im Voraus berechnet werden. Dabei ist shif t[x] als |w| − |u| definiert, wobei u das längste Suffix von x ist, das ein echtes Präfix von w ist. u ist dabei in jedem Schritt bekannt, denn es korrespondiert mit dem letzte Knoten auf dem betrachteten Pfad in DAW G(w), der zu einem Suffix von wR gehört. Der Algorithmus BDM arbeitet sowohl für kleine als auch für große Alphabete im Durchschnitt sehr schnell. Im besten Fall arbeitet er bei einem Alphabet der Größe 2 sogar in O(nlog(m)/m). Im schlimmsten Fall hat er jedoch quadratische Laufzeit. Durch einige Verbesserungen, die ähnlich denen vom Boyer-Moore- auf den Turbo-Boyer-MooreAlgorithmus sind, kann man die Laufzeit so verbessern, dass sie auch im schlechtesten Fall linear ist und im Durchschnitt immer noch optimal. Auch für den Beweis sei hierzu auf die Originalliteratur verwiesen. 44 5 Implementierung eines DAWGs und Vergleich mit einem Suffix-Baum In diesem Kapitel soll eine Möglichkeit vorgestellt werden, wie man DAWGs konkret implementieren kann. Anschließend soll anhand verschiedener Textbeispiele ein Vergleich mit einer Implementierung eines Suffix-Baumes angestellt werden. Dabei sollen die Laufzeit, der Speicherplatzbedarf sowie die Anzahl der Objekte, die von beiden Algorithmen erstellt werden, betrachtet werden. Für den Vergleich wird eine bereits vorhandene Implementierung von Suffix-Bäumen verwendet, wie sie in der Diplomarbeit von Olesia Brill aus dem Jahr 2009 vorstellt wird [?]. Der dort erstellte Algorithmus wurde in Java geschrieben, weshalb auch die im Folgenden näher vorgestellte Implementierung der DAWGs in Java vorgenommen wurde. An dieser Stelle sei bereits darauf hinzuweisen, dass Java für Laufzeitberechnungen, verglichen etwa mit der Programmiersprache C, deutlich schlechter geeignet ist. Bei der Implementierung der Suffix-Bäume sowie auch der DAWGs wurde deswegen darauf geachtet, dass möglichst wenig laufzeitintensive Elemente aus Java verwendet wurden. Für die Ausführung der beiden Algorithmen und die Vergleiche wurde ein PC mit Intel Core 2 Duo (3 GHz) mit 4GB RAM unter Windows 7 RC Build 7100 (64 Bit) verwendet. Die Java-Version war dabei Java Standard Edition 6. 5.1 Grundlegender Aufbau des Algorithmus Der Algorithmus unterteilt sich in vier Packages: • de.luh.psue.algorithm, welches das Interface IClassificationAlgorithm und die Klasse ClassificationAlgorithm enthält, • de.luh.psue.dawg, in dem die gerichteten azyklischen Wortgraphen mit den beiden Klassen DAWG und DAWGNode implementiert wurden, • de.luh.psue.suffixTree mit den Klassen FasterSuffixTree und SuffixTreeNode für die Darstellung der Suffix-Bäume, 45 • de.luh.psue.statistics, in dem Zufallstexte mit RandomText erzeugt und die Laufzeitberechnungen mithilfe von TestAlgorithms und ResultStore durchgeführt und gespeichert werden. Um die Vergleichbarkeit der beiden Algorithmen zu gewährleisten, erben die beiden Klassen FasterSuffixTree und DAWG von der abstrakten Klasse ClassificationAlgorithm, die wiederum ein gemeinsames Interface IClassificationAlgorithm implementiert. IClassificationAlgorithm wurde in leicht abgewandelter Form aus der Arbeit von Frau Brill übernommen, weshalb das Interface neben den benötigten Methoden auch noch einige weitere enthält, die im Rahmen der Messungen dieser Arbeit nicht verwendet werden. Es stellt die Methoden bereit, mit denen die Algorithmen erzeugt werden können. Es können bestimmte Pattern- oder Wortlängen angegeben werden, die in den Diagrammen der späteren Auswertungen dargestellt werden sollen (interestingPatternLengths und interestingWordLengths), ebenso wie eine maximale Patternlänge vorgegeben werden kann (maximalPatternLenght. Für die Messungen dieser Arbeit ist nur der Wert für verschiedene Wortlängen von Interesse. Mit den Methoden setAlphabet, getAlphabet, setPatternLength und getPatternLength werden das Alphabet und die Länge der zu betrachtenden Pattern festgelegt, wobei auch hier für den Rahmen der Messungen dieser Arbeit nur die Methoden für das Festlegen des Alphabets relevant sind. Mit setWord bzw. setWords und getWord werden die Worte definiert, zu denen der DAWG und der Suffix-Baum konstruiert werden sollen. construct stellt die eigentliche Methode der Konstruktion der Datenstrukturen dar, clear sorgt dafür, dass nach dem Erzeugen der benötigte Speicherplatz vollständig wieder freigegeben wird. Die Klasse ClassificationAlgorithm dient im Wesentlichen dazu, einen oder mehrere übergebene Texte einzulesen und in der Methode construct die Algorithmen mit dem entsprechenden Wort aufzurufen. Die Repräsentation des Wortes wird dabei intern nicht durch einzelne Buchstaben, sondern durch Zahlen vorgenommen. Das hat unter anderem Vorteile für die Nachfolgerdarstellung, wie sie in 5.2 näher beschrieben wird. Die Methode buildAlphabet wandelt ein übergebenes Alphabet alphabet in dessen interne Darstellung invAlphabet um, in der die Buchstaben des Alphabets durch die IndexWerte des Arrays dargestellt werden, also das erste Zeichen des Alphabets durch eine 0 repräsentiert wird, das zweite durch eine 1 usw. In setWords wird anschließend das Wort aus der Datei bzw. den Dateien eingelesen und mithilfe von invAlphabet so umgewandelt, dass jeder Buchstabe des Alphabets in dem Wort durch dessen Zahlenrepräsentation dargestellt wird. So wird zum Beispiel aus einem Wort w = abaab mit einem Alphabet mit den beiden Zeichen a und b das Wort 01001. Abbildung 5.1 zeigt die Klassen IClassificationAlgorithm, ClassificationAlgorithm, DAWG und FasterSuffixTree und die Beziehung zwischen ihnen. 46 Abbildung 5.1: Darstellung der Algorithmen und Vererbungsstruktur 47 5.2 Nachfolgerimplementierung Ein DAWG besteht im Wesentlichen aus Kanten und Knoten, wobei die Knoten jeweils eine bestimmte Anzahl von Nachfolgern haben. Die wichtigste Frage für die Implementierung ist also, wie die Darstellung der Knoten und Kanten und insbesondere der Nachfolger realisiert werden kann. Hierfür gibt es verschiedene Ansätze, die wesentliche Unterschiede sowohl für die Implementierung als auch für die Laufzeit und den Speicherplatzbedarf ergeben. So wären etwa HashMaps, verkettete Listen, Arrays oder balancierte Suchbäume denkbare Implementierungsvarianten. Jede dieser Varianten hat, je nach Anwendungsgebiet, unterschiedliche Vor- und Nachteile. Die hier vorgestellte Implementierung des DAWGs verwendet für die Nachfolgerdarstellung Arrays. Die Entscheidung zu dieser Datenstruktur wurde hauptsächlich getroffen, weil die Implementierung der Suffix-Bäume, die für die Vergleiche verwendet wird, ebenfalls mit dieser Struktur arbeitet, was bessere Vergleichbarkeit ermöglicht. Da im DAWG die Nachfolger von jedem Knoten eindeutig durch die Markierungen der Kanten, die zu ihnen führt, bestimmt werden, also jeder Knoten keine zwei Kanten mit derselben Markierung haben kann, kann in einem Array zu jedem Zeichen aus dem Alphabet entweder ein Knoten gespeichert sein oder nicht. Durch die Darstellung der Buchstaben des zu bearbeitenden Wortes als Index-Werte kann in konstanter Zeit auf einen bestimmten Index-Wert des Arrays und somit auf einen bestimmten Nachfolger zugegriffen werden. Ein wesentlicher Nachteil der Verwendung von Arrays ist der hohe Speicherplatzbedarf. Speziell für große Alphabete (also etwa das ASCII-Alphabet) benötigt man viel Speicherplatz für das Array, auch wenn letztendlich nur wenige Werte darin gespeichert werden müssen. Bei einem Knoten, der nur einen Nachfolger hat, würde man so trotzdem ein Array für das komplette Alphabet benötigen. 5.3 Implementierung des DAWGs In der Klasse DAWGNode wird ein Knoten des DAWGs mit seinen Nachfolgern realisiert. Die Klasse wird in Abbildung 5.2 dargestellt. Da für den Konstruktionsalgorithmus zwei Kantenarten unterschieden werden, sind in den Arrays primarySuccessors und secundarySuccessors die Nachfolger gespeichert, die über eine primäre bzw. sekundäre Kante mit dem aktuellen Knoten verbunden sind. In suffixLink wird der Knoten gespeichert, der vom aktuellen Knoten über einen Suffix-Link erreicht wird. Jeder Knoten besitzt außerdem eine eindeutige Nummer, die insbesondere für die Ausgabe des DAWGs durch die Methode display() aus der Klasse DAWG von Bedeutung ist. Die Methode copyEdgesToSecundary dient dazu, alle primären Kanten des aktuellen Knotens in sekundäre Kanten für einen übergebenen Knoten newNode umzuwandeln. Diese Methode wird innerhalb des split im Konstruktionsalgorithmus aufgerufen. Durch 48 Speichern der Werte firstPrimarySuccessor und lastPrimarySuccessor für den ersten und letzten eingetragenen Nachfolger kann hier minimal Zeit beim Durchlaufen des Arrays gespart werden. Abbildung 5.2: Die Klasse DAWGNode Die Klasse DAWG realisiert die eigentliche Konstruktion des DAWGs. Sie orientiert sich dabei stark an dem Algorithmus aus [?], wie er in Kapitel 4.3 beschrieben wurde. Die Methode construct erzeugt für das zu bearbeitende Wort den DAWG, indem es von links nach rechts die Zeichen durchläuft und den Graphen entsprechend erweitert. Um den verwendeten Speicherplatz vollständig wieder freizugeben, wird in clear bzw. clearNode dafür gesorgt, dass alle Werte in den Arrays sowie alle Knoten gelöscht werden. display stellt eine stark vereinfachte Ausgabe des DAWGs dar, wobei hier die Nummern der Knoten als Repräsentanten ausgegeben werden. Um die Nummern der Knoten festlegen und die Anzahl nachher bestimmen zu können, wird durch nodeCount gezählt, wie viele Knoten bisher erzeugt wurden. 5.4 Implementierung der Suffix-Bäume Die Implementierung der Suffix-Bäume wird in ausführlicher Form in [?] beschrieben, weshalb hier nur wesentliche Änderungen kurz erläutert werden sollen. Der verwendeten Klasse FasterSuffixTree wurde die Methode display hinzugefügt, um für Versuchszwecke eine Ausgabe zu bekommen. Wie auch in der Klasse DAWG wird display für die Laufzeitberechnungen nicht verwendet. Die Original-Klasse FasterSuffixTree enthält diverse Methoden, die für das ursprüngliche Anwendungsgebiet notwendig, für die hier angestellten Vergleiche allerdings nicht 49 notwendig sind. Diese Methoden wurden entfernt. Außerdem wurden einige Aufrufparameter entfernt und daraus resultierend einige kleinere Änderungen an der Methode construct vorgenommen, die sich aber nicht auf die Laufzeit oder den Speicherbedarf des Algorithmus auswirken. Die Klasse SuffixTreeNode ist eine leicht modifizierte Variante der Original-Klasse Node. Auch hier wurden einige Methoden und Variablen entfernt, die für den Vergleich keine Rolle mehr spielen, ansonsten wurde die Klasse beibehalten. Abbildung 5.3 stellt die Klasse in UML dar. Abbildung 5.3: Die Klasse SuffixTreeNode 5.5 Laufzeitberechnungen und Zufallstexte Für die Berechnungen von Laufzeit und Speicherplatzbedarf wurden drei Klassen verwendet, die sich im Package de.luh.psue.statistics befinden. Die Klasse RandomText erzeugt für ein gegebenes Alphabet einen Zufallstext von vorher festgelegter Länge. Die Konstruktion der Zufallstexte mit dem Java-Zufallsgenerator ist, wie auch in der Arbeit von Frau Brill thematisiert, nicht unbedingt optimal, da die auf diese Weise erzeugten Texte nicht vollständig zufällig sind und gewisse Anomalitäten aufweisen. 50 TestAlgorithms führt Tests anhand verschiedener Texte und Alphabete durch und stellt die Ergebnisse mit Hilfe der Java-Bibliothek JFreeChart-1.0.12 in Form von Diagrammen dar. Die Klasse wurde aus der Arbeit von Frau Brill übernommen und für die Zwecke dieser Arbeit modifiziert. Sämtliche Tests messen den verwendeten Speicherplatz, die Laufzeit sowie die Anzahl der Objekte, die von den Algorithmen erzeugt wurden. In der Methode void testAlgorithm werden Texte mit gleicher Länge und verschiedenen Alphabetgrößen jeweils einzeln für DAWG und Suffix-Baum getestet, sodass je ein Diagramm für jede der Datenstrukturen erstellt wird. Dort ist auf der x-Achse die Textlänge aufgetragen, auf der y-Achse Speicherbedarf, Laufzeit bzw. Objektanzahl. Die einzelnen Kurven stellen die unterschiedlichen Alphabetgrößen dar. compareAlgorithms vergleicht beide Algorithmen für verschiedene Texte und Textlängen und stellt die Ergebnisse so dar, dass in einem Diagramm die Ergebniskurven beider Algorithmen stehen. Auf der x-Achse ist dabei die Textlänge bzw. die Alphabetgröße aufgetragen, die y-Achse enthält Speicherbedarf, Laufzeit oder Objektanzahl. Die Methode AlgorithmResult testAlgorithm wird aus den oben genannten Methoden aufgerufen und führt die eigentlichen Messungen durch. Dabei wird zunächst mithilfe von freeMemory dafür gesorgt, dass alle nicht benötigten Objekte aus dem Speicher gelöscht werden. Anschließend wird der Speicherplatzbedarf sowie die Zeit vor Ausführung des jeweiligen Algorithmus gespeichert, dann mit der Methode construct der DAWG bzw. Suffix-Baum konstruiert und schließlich erneut der Speicherplatzbedarf und die Zeit bestimmt, um daraus die Laufzeit und den genauen Bedarf an Speicher zu berechnen. In der Klasse ResultStore werden die Ergebnisse, die zuvor berechnet wurden, gespeichert. Die Klasse bekommt die Ergebnisse direkt aus der Klasse TestAlgorithm und erzeugt eine Datei für die Messwerte, deren Name aus dem Rechnernamen gebildet wird. In der Klasse kann überprüft werden, ob der jeweils betrachtete Messwert sich geändert hat oder nicht. Werte werden für eine einstellbare Zeit gespeichert, das heißt, es können auch bereits berechnete Werte für die Auswertungen benutzt werden. Das ResultStoreObjekt wird bei Ausführen von TestAlgorithms geladen, wenn es vorhanden ist, und je nachdem, wie alt die Werte in der Datei sind, wird entschieden, ob neu gemessen werden muss oder die bisherigen Werte verwendet werden können. 51 6 Messergebnisse und Auswertungen In diesem Kapitel sollen die Ergebnisse der Messungen dargestellt und erläutert werden, die mit den beiden Implementierungen durchgeführt wurden. Die Tests werden in drei verschiedene Bereiche eingeteilt: Tests mit Zufallstexten, Tests mit realen Texten und Tests mit DNA-Strängen. Bei den Zufallstexten handelt es sich um Texte, die mit dem Zufallsgenerator RandomText zu unterschiedlichen Alphabeten und in unterschiedlicher Länge erzeugt wurden. Reale Texte stellen Ausschnitte aus Texten bzw. Büchern dar, die online frei zugängig sind. Hierbei werden nur verschiedene Textlängen betrachtet, das Alphabet soll in allen Fällen das komplette ASCII-Alphabet sein. Für DNA-Stränge werden unterschiedlich lange Sequenzen von DNA betrachtet. Das Alphabet ist in diesem Fall auf die 4 Symbole A, C, G und T, entsprechend den vier Basen der DNA, beschränkt. Die Tests als solche messen sowohl die Laufzeit als auch den Speicherbedarf und die Zahl der erzeugten Objekte. Als Objekte werden hier die Java-Objekte gezählt, also für den DAWG die Objekte der Klasse DAWGNode und für den Suffix-Baum die der Klasse SuffixTreeNode. 6.1 Tests mit Zufallstexten In diesem Abschnitt sollen die Ergebnisse der Tests mit Zufallstexten vorgestellt und erläutert werden. Hierzu wurden anhand von Zufallstexten mit verschiedenen Alphabetgrößen Speicherbedarf, Laufzeit und Objektanzahl für den Suffix-Baum und den DAWG separat gemessen. Betrachtet man zunächst die Ergebnisse der Messungen des Speicherbedarfs (Abbildung 6.1 und 6.2), stellt man fest, dass sowohl für den DAWG als auch für den Suffix-Baum wie erwartet mit steigender Textlänge der benötigte Speicherplatz streng linear ansteigt, da für längere Texte mehr Knoten erzeugt und gespeichert werden müssen. Vergrößert sich das Alphabet, müssen die Arrays vergrößert werden, in denen die Nachfolger für die Knoten der beiden Algorithmen gespeichert werden. Das hat zur Folge, dass auf diese Weise der Speicherplatzbedarf bei gleicher Textlänge drastisch ansteigt. Vergleicht man die absoluten Werte in beiden Abbildungen, erkennt man, dass bei gleicher Alphabetgröße der Speicherplatzbedarf für den DAWG in etwa doppelt so hoch ist wie für den Suffix-Baum. Noch besser erkennbar ist diese Beobachtung bei den direkten 52 Abbildung 6.1: Speicherbedarf DAWG für verschiedene Alphabetgrößen in Abhängigkeit von der Textlänge Abbildung 6.2: Speicherbedarf SuffixTree für verschiedene Alphabetgrößen in Abhängigkeit von der Textlänge 53 Vergleich beider Algorithmen, wie er in Abbildung 6.3 zu sehen ist. Dort wurden die Messungen anhand eines Zufallstextes mit zwei Millionen Zeichen bei einer Alphabetgröße von 4 in Abhängigkeit von der Textlänge durchgeführt. Auch hier ist eindeutig erkennbar, dass der DAWG deutlich mehr Speicherplatz benötigt als der Suffix-Baum. Abbildung 6.3: Vergleich des Speicherbedarfs beider Algorithmen für einen Text mit Alphabetgröße 4 in Abhängigkeit der Textlänge Der Grund für diese Ergebnisse liegt in der Implementierung der beiden Datenstrukturen. Im Suffix-Baum sind die Nachfolger durch ein einzelnes Array implementiert, das in jedem Knoten gespeichert ist. Beim DAWG wurden in der hier vorgestellten Implementierung ebenfalls Arrays verwendet, wobei hier nicht ein einzelnes Array, sondern zwei verwendet wurden. Auf diese Weise werden Nachfolger, die über primäre Kanten erreicht werden können, von solchen, die über sekundäre Kanten erreicht werden können, unterschieden. Der Nachteil dieser Implementierungsentscheidung ist, dass man auf diese Weise beim DAWG doppelt so viel Speicherplatz für die Nachfolgerrealisierung benötigt als beim Suffix-Baum. Betrachtet man die Anzahl der Objekte, die von DAWG und Suffix-Baum unter verschiedenen Alphabetgrößen erzeugt werden, fällt auf, dass hier das Absolutwert für das kleinste Alphabet (also das mit vier Zeichen) der größte ist (Abbildung 6.4 und 6.5). Beim direkten Vergleich der beiden Datenstrukturen erkennt man erst bei sehr langen Texten, dass der DAWG mehr Objekte erzeugt, bei kürzeren Texten sind die Unterschiede hier noch minimal. Abbildung 6.6 zeigt die Anzahl der erzeugten Objekte beider Algorithmen im Vergleich. 54 Abbildung 6.4: Erzeugte Objekte von DAWG für verschiedene Alphabetgrößen in Abhängigkeit von der Textlänge Abbildung 6.5: Erzeugte Objekte von SuffixTree für verschiedene Alphabetgrößen in Abhängigkeit von der Textlänge 55 Abbildung 6.6: Vergleich der Anzahl der erzeugten Objekte beider Algorithmen für einen Text mit Alphabetgröße 4 in Abhängigkeit der Textlänge Diese Ergebnisse scheinen zunächst etwas überraschend, denn im gerichteten azyklischen Wortgraphen hätte man durch das Zusammenfassen isomorpher Teilbäume vermuten können, dass dieser weniger Knoten und somit weniger Objekte benötigt als der SuffixBaum. Da im Suffix-Baum aber Kantenmarkierungen zusammengefasst werden können, während im DAWG jede Kante nur ein einzelnes Zeichen enthalten kann, enthält der DAWG letztendlich trotzdem mehr Objekte als der Suffix-Baum, wenn auch wie erwähnt der Unterschied besonders bei kleineren Textgrößen nicht allzu gravierend ist. Schaut man sich die Laufzeiten der beiden Algorithmen an, fällt zunächst auf, dass verglichen mit der Objektanzahl und dem Speicherplatzbedarf die Kurven deutlich „welliger“ sind (siehe Abbildung 6.7 und 6.8). Diese Schwankungen sind vermutlich auf den Java Garbage Collector zurückzuführen, da sie keinerlei Regelmäßigkeiten erkennen lassen und bei beiden Datenstrukturen auftreten. Es wäre auch denkbar, dass hier im Text bestimmte Kombinationen von Zeichen auftreten, die einen höheren oder geringeren Aufwand für das Einfügen benötigen, doch in diesem Fall würden die Schwankungen wohl nicht derart stark ausgeprägt sein. Sieht man davon ab, lässt sich erkennen, dass die Laufzeit linear in Abhängigkeit von der Textlänge ansteigt. Wie auch beim Speicherbedarf ist dieses Ergebnis leicht erklärbar, denn durch die erhöhte Textlänge wird mehr Zeit benötigt, um einen neuen Knoten einzufügen. Erhöht sich die Alphabetgröße bei gleicher Textlänge, so steigt die Laufzeit ebenfalls. Das ist damit zu begründen, dass in diesem Fall ein höherer Aufwand benötigt wird, um 56 Abbildung 6.7: Laufzeit DAWG für verschiedene Alphabetgrößen in Abhängigkeit von der Textlänge Abbildung 6.8: Laufzeit SuffixTree für verschiedene Alphabetgrößen in Abhängigkeit von der Textlänge 57 die Arrays zu konstruieren und Elemente einzufügen bzw. aufzurufen. Vergleicht man die Laufzeit beider Algorithmen direkt (siehe Abbildung 6.9), fällt auf, dass der DAWG deutlich langsamer konstruiert wird als der Suffix-Baum. Auch hier ist der Grund dazu in der Implementierung der beiden Datenstrukturen zu finden. Die Implementierung der Suffix-Bäume wurde in Hinblick auf die Laufzeit so optimiert, dass sie keine oder nur sehr wenige laufzeitintensive Methoden oder Strukturen von Java verwendet. Auch wenn beim DAWG versucht wurde, den Programmcode weitestgehend zu optimieren, ist das aufgrund mangelnder Kenntnisse über das Laufzeitverhalten in Java sicherlich nicht an jeder Stelle optimal gelungen. Hinzu kommt noch, dass im Konstruktionsalgorithmus des DAWGs an mehreren Stellen Arrays durchlaufen werden müssen, um bestimmte Kanten zu finden oder alle Kanten zu kopieren, was die Laufzeit außerdem noch steigert. Abbildung 6.9: Vergleich der Laufzeiten beider Algorithmen für einen Text mit Alphabetgröße 4 in Abhängigkeit der Textlänge Interessante Ergebnisse erzielt man bei den Messungen, wenn man Zufallstexte einer bestimmten Länge erzeugt und hintereinander in eine Datei kopiert, auf der man anschließend die Messungen durchführt. Für die in den Abbildungen 6.10, 6.11 und 6.13 dargestellten Ergebnisse wurde ein Zufallstext der Länge 50000 erstellt und zehnmal hintereinander in eine Datei kopiert. Betrachtet man zunächst den Speicherbedarf (Abbildung 6.10), fällt auf, dass dieser bis zur Textlänge 50000 wie erwartet linear steigt, sowohl beim Suffix-Baum als auch beim DAWG. Ab diesem Wert, das heißt, sobald im Text die Wiederholung der ersten 50000 58 Zeichen auftritt, erkennt man beim DAWG einen leichten Knick beim Speicherbedarf, danach steigt die Kurve wieder ohne Knicke an. Abbildung 6.10: Vergleich des Speicherbedarfs beider Algorithmen an einem hintereinander kopierten Zufallstext Aufgrund der Wiederholung der ersten Zeichen des Textes müssen ab dem Wert von 50000 nicht mehr so viele Knoten in den Graphen eingefügt werden. Die Suffixe der Anfangszeichen im Text werden bereits dargestellt, sodass nur noch die längeren Suffixe eingefügt werden müssen. Da diese aber durch Verbindungen zu den vorhandenen Knoten realisiert werden können, verringert sich die Zahl neuer Knoten in jedem Schritt. Beim Suffix-Baum erkennt man in Abbildung 6.10, dass auch hier ab dem Wert von 50000 ein Knick auftritt. Im Gegensatz zum DAWG wird ab diesem Punkt jedoch kein weiterer Speicherplatz in Anspruch genommen. Betrachtet man dazu auch noch die Zahl der Objekte in Abbildung 6.11, sieht man, dass auch hier im Gegensatz zum DAWG keine neuen Objekte mehr erzeugt werden, die Zahl also von da an konstant bleibt. Um dieses Phänomen zu erklären, muss man auf die Besonderheiten der Konstruktion des Suffix-Baumes bei Ukkonen schauen. Wichtig ist zunächst, dass nach dem Algorithmus von Ukkonen implizite Suffix-Bäume erzeugt werden, bei denen nicht jedes Suffix zwangsläufig durch ein Blatt dargestellt werden muss. Stattdessen gilt hier nur, dass jedes Suffix ein Präfix einer Zeichenkette ist, die durch einen Knoten im Baum dargestellt wird. Wie in Kapitel 3.2 erwähnt, gibt es im Ukkonen-Algorithmus mehrere Regeln, nach denen Suffixe in den Baum eingefügt werden. Die letzte dieser Regeln ist hier die interessante. Dort wird, wenn ein Weg so endet, dass eine Fortsetzung mit w[i] möglich ist, kein neuer 59 Abbildung 6.11: Vergleich der Anzahl der erzeugten Objekte beider Algorithmen an einem hintereinander kopierten Zufallstext Knoten in den Baum eingefügt. Bei den vorliegenden Messungen tritt dieser Fall ab der Stelle, an der sich die ersten 50000 Zeichen des Textes wiederholen, in jedem Schritt auf. Nach dem Algorithmus werden also nur die Kantenmarkierungen erweitert, neue Knoten bzw. neue Objekte werden nicht erzeugt. Die folgende Abbildung 6.12 zeigt den Ablauf des Algorithmus für ein Beispielwort w = abcabc. Man erkennt dort, dass nach der Konstruktion des Baumes für w[1 . . . 3] = abc keine neuen Knoten mehr eingefügt werden. Stattdessen werden lediglich die Kantenmarkierungen erweitert. Auf diese Weise werden im Suffix-Baum für w = abcabc nicht alle Suffixe von w durch Blätter dargestellt, aber da es sich um einen impliziten Suffix-Baum handelt, ist das auch nicht zwangsläufig gefordert. So ist es also zu erklären, dass der DAWG für den kompletten Text neue Knoten erzeugen muss, der Suffix-Baum jedoch nicht. Auch die Kurve der Laufzeitmessung, die in Abbildung 6.13 zu sehen ist, verläuft entsprechend beim DAWG so, dass die nach der Grenze von 50000 Zeichen etwas schwächer steigt, beim Suffix-Baum dagegen nahezu konstant bzw. linear mit nur sehr geringer Steigung. Die „Stufe“ in der Laufzeitkurve des Suffix-Baumes bei großer Textlänge ist vermutlich ebenso wie die Schwankungen in der Laufzeit in vorigen Tests durch ein Java-internes Phänomen bzw. durch den Garbage Collector zu erklären. 60 a ab abca abcabc bca bcabc abc b ca abcab bc bcab c cab cabc Abbildung 6.12: Beispielhafte Konstruktion des Suffix-Baumes zu w = abcabc nach dem Algorithmus von Ukkonen Abbildung 6.13: Vergleich der Laufzeit beider Algorithmen an einem hintereinander kopierten Zufallstext 61 6.2 Tests mit realen Texten Dieser Abschnitt stellt die Messergebnisse dar, die sich beim Testen der Algorithmen für den Suffix-Baum und den DAWG anhand von realen Texten ergeben haben. Als Textgrundlage sollen hier unterschiedliche Texte dienen, die aus den Online-Quellen [?] und [?] entnommen wurden. Betrachtet man zunächst Robinson Crusoe von Daniel Defoe (ca. 450000 Zeichen) und dort zu allererst den Speicherbedarf, sind hier vom Kurvenverlauf her keine nennenswerten Unterschiede zu den Zufallstexten erkennbar. Der Kurvenverlauf ist streng linear und der DAWG benötigt verglichen mit dem Suffix-Baum etwa doppelt so viel Speicherplatz (siehe Abbildung 6.14). Abbildung 6.14: Speicherbedarf der Algorithmen bei Robinson Crusoe Vergleicht man diesen realen Text mit einem Zufallstext mit gleicher Alphabetgröße, fällt auf, dass beim realen Text von beiden Algorithmen absolut mehr Speicherplatz benötigt wird. So benötigt der Suffix-Baum bei 400000 Zeichen Textlänge bei einem Zufallstext etwa 500MB Speicher, bei Robinson Crusoe etwas mehr als 600MB, der DAWG beim Zufallstext etwas weniger als 1000MB, beim realen Text knapp 1200MB. Abbildung 6.15 stellt die Messungen des Speicherbedarfs des Zufallstextes dar. Schaut man sich zu den realen Texten die Zahl der erzeugten Objekte und die Laufzeit an, ist auch hier vom Kurvenverlauf her kein wesentlicher Unterschied zu den Zufallstexten festzustellen. Betrachtet man die Objektanzahl beispielsweise anhand von The Time Machine von H.G.Wells, so sieht man auch dort, dass die Zahl der Objekte bei beiden Algorithmen nahezu gleich ist und nur bei größeren Textlängen vom DAWG minimal 62 Abbildung 6.15: Speicherbedarf der Algorithmen an einem Zufallstext mit Alphabetgröße 255 mehr Objekte erzeugt werden. Bei Robinson Crusoe werden sogar mehr Objekte vom Suffix-Baum als vom DAWG konstruiert, der Unterschied ist aber in absoluten Werten auch hier nur sehr gering. Die Abbildungen 6.16 und 6.17 stellen die Objektzahlen der beiden Texte dar. Beim Vergleich der absoluten Werte werden hier beim Zufallstext deutlich weniger Objekte erzeugt, was auch erklärt, wieso der Speicherbedarf geringer ist. In Abbildung 6.18 ist beispielhaft die Laufzeit der Algorithmen beim Text The Time Machine dargestellt. Verglichen mit dem Zufallstext mit gleichem Alphabet ist die Laufzeit absolut betrachtet höher, was aber ebenso an der höheren Objektzahl des realen Textes liegt. 63 Abbildung 6.16: Objektzahl in Abhängigkeit von der Textlänge bei Robinson Crusoe Abbildung 6.17: Objektzahl in Abhängigkeit von der Textlänge bei The Time Machine 64 Abbildung 6.18: Laufzeit in Abhängigkeit von der Textlänge bei The Time Machine 6.3 Tests mit DNA-Strängen Dieser Abschnitt befasst sich mit Tests der beiden Algorithmen an DNA-Strängen. Grundlage für die Messungen sind dabei die DNA-Stränge mehrerer Bakterienarten, die auf [?] frei zugänglich sind. Dabei wurden die Arten der Bakterien zufällig gewählt, einzig die Länge der DNA sollte sich deutlich unterscheiden. Schaut man sich die Ergebnisse an, gibt es auch hier kaum nennenswerte Besonderheiten zu erkennen. Sowohl der Speicherbedarf als auch die Objektzahl verhalten sich wie bei Zufallstexten und realen Texten, sogar die absoluten Werte im Direktvergleich zwischen einem Zufallstext mit Alphabetgröße 4 und einem DNA-Strang sind nahezu identisch. Einzig auffällig ist, dass die Laufzeitkurve deutlich weniger wellig ist als bei den Zufallstexten und realen Texten, das heißt, dass die Zeit für die Konstruktion deutlich weniger Schwankungen ausgesetzt ist. Ob das allerdings im direkten Zusammenhang mit der Art des Textes steht, ist eher fraglich. Wahrscheinlicher ist es, dass auch hier wieder der Garbage Collector sowie Ungenauigkeiten bei der Messung zu den Unterschieden führen. In den Abbildungen 6.19 bis 6.21 sind die Ergebnisse beispielhaft für den Lactobacillus casei dargestellt, das Verhalten bei anderen Bakterienarten ist nahezu identisch dazu. 65 Abbildung 6.19: Speicherbedarf der Algorithmen bei einem DNA-Strang Abbildung 6.20: Objektanzahl der Algorithmen bei einem DNA-Strang 66 Abbildung 6.21: Laufzeit der Algorithmen bei einem DNA-Strang 6.4 Bewertung der Ergebnisse Die Ergebnisse aus den vorigen Abschnitten zeigen, dass bei allen drei getesteten Textformen der DAWG in der hier vorgestellten Implementierung schlechtere Ergebnisse als die Implementierung des Suffix-Baumes liefert. Bei der Laufzeit ist das darauf zurückzuführen, dass die Implementierung des DAWGs nicht in dieser Hinsicht optimiert ist. Ebenso wäre durch eine Änderung der Nachfolgerrealisierung beim DAWG ein geringerer Speicherplatzbedarf zu erreichen. Was die Anzahl der Objekte angeht, zeigt sich bei den Messungen, dass trotz des Zusammenfassens isomorpher Teilbäume im DAWG nur in vereinzelten Fällen weniger Knoten im Graphen erzeugt werden als im Suffix-Baum. Wie bereits früher in diesem Kapitel erwähnt, ist dies auf das Zusammenfassen der Kantenmarkierungen im Suffix-Baum zurückzuführen. Aus den Messungen geht ebenso hervor, dass man bei speziell konstruierten Texten deutliche Vorteile mit dem Suffix-Baum hat. Am Beispiel des hintereinander kopierten Zufallstextes ist deutlich geworden, dass hier der Algorithmus von Ukkonen durch die Verwendung impliziter Suffix-Bäume den DAWG in Laufzeit, Objektzahl und Speicherbedarf deutlich übertrifft. Diese besondere Struktur ist jedoch bei realen Texten oder DNA-Strängen nie gegeben, sodass der Vorteil eher aus akademischer Sicht interessant ist als aus praktischer. Im Vergleich der unterschiedlichen Textarten ist deutlich geworden, dass es weder bei 67 der Laufzeit noch beim Speicherbedarf oder der Objektzahl deutliche Unterschiede zwischen realen Texten, Zufallstexten und DNA-Strängen gibt. Zwar konnten zwischen den Zufallstexten und den realen Texten kleinere Unterschiede festgestellt werden, ob diese allerdings repräsentativ sind, lässt sich anhand der hier durchgeführten Messungen nicht allgemein sagen. 68 7 Fazit und Ausblick Die Suche nach Pattern in feststehenden Texten ist ein Problem, dass nach wie vor sehr aktuell ist. Gerade in der Genetik, wo man es mit langen DNA-Sequenzen zu tun hat, steht man weiterhin vor dem Problem, wie man dort schnell nach bestimmten Teilsequenzen suchen kann. DAWGs und Suffix-Bäume sind dabei sicherlich mögliche Ansätze, sie sind jedoch nur als eine Art Oberklasse von Strukturen zu betrachten. Mittlerweile existieren sowohl für DAWGs als auch für Suffix-Bäume diverse Varianten und unterschiedliche Algorithmen. So gibt es für Suffix-Bäume etwa den Algorithmus von Farach [?] und als Variante für DAWGs unter anderem die CDAWGs, wie sie in Kapitel 4 kurz erwähnt wurden. Neben den beiden hier vorgestellten Datenstrukturen existieren, wie schon in Kapitel 3.3 erwähnt, noch verschiedene andere Strukturen. So seien an dieser Stelle etwa SuffixArrays, Suffix-Vektoren und Suffix-Kakteen genannt. Beim Vergleich der Objektzahlen der DAWGs und Suffix-Bäumen in Kapitel 6 dieser Arbeit ist aufgefallen, dass DAWGs nur unwesentlich mehr Objekte benötigen als SuffixBäume, auch wenn in den Suffix-Bäumen Kantenmarkierungen zusammengefasst sind, während sie bei DAWGs nur aus einzelnen Zeichen bestehen. Hier wäre es interessant, einen Vergleich mit einer Implementierung von CDAWGs durchzuführen, in denen ähnlich wie in Suffix-Bäumen Kantenmarkierungen zusammengefasst werden können. Auch die Frage, ob CDAWGs eine Alternative zu DAWGs darstellen und ob sie in der Theorie sowie in der Praxis vielleicht sogar besser geeignet sind als DAWGs, wäre in einer weiterführenden Arbeit interessant zu betrachten. Wie bereits in Kapitel 5 und 6 angesprochen, spielt es bei allen Datenstrukturen eine entscheidende Rolle, wie sie im Detail implementiert wurden. Auch wenn für die in dieser Arbeit vorgestellte Implementierung schon einige Optimierungen getroffen wurden, kann davon ausgegangen werden, dass man die Performance durch weitere Optimierungsmaßnahmen noch weiter steigern könnte. Sicherlich wäre es in diesem Zusammenhang auch sinnvoll, verschiedenen Implementierungen von DAWGs oder Suffix-Bäumen mit unterschiedlichen Datenstrukturen für die Nachfolger miteinander zu vergleichen. Zu erwarten wäre hierbei, dass abhängig von der Alphabetgröße deutliche Unterschiede hinsichtlich von Laufzeit und Speicherplatzbedarf festgestellt werden könnten. Fraglich ist auch, ob eine optimierte Variante der DAWG-Implementierung bei bestimmten Texten eine bessere Performance erreichen würde als die Suffix-Baum-Implementierung, auch wenn das 69 nach den Messungen aus Kapitel 6, in denen die Unterschiede doch gravierend waren, eher unwahrscheinlich erscheint. In der Praxis haben sich in letzter Zeit die Suffix-Arrays immer mehr gegenüber den Suffix-Bäumen und DAWGs durchgesetzt. Besonders die einfache Konstruktion und die kompakte Darstellung der Suffix-Arrays haben dafür gesorgt, dass die anderen Strukturen speziell im Bereich der Bio-Informatik mehr und mehr abgelöst wurden. Nach wie vor gibt es aber nicht die eine Datenstruktur, die für alle Anwendungsgebiete optimal ist. Aus diesem Grund ist auch davon auszugehen, dass besonders wegen der zunehmenden Bedeutung von DNA und der damit verbundenen Notwendigkeit von Suchalgorithmen auch in Zukunft weiterhin nach Datenstrukturen oder Konstruktionsmöglichkeiten gesucht werden wird, um Performance-Verbesserungen zu erreichen. 70 Literaturverzeichnis [AHU 74] Aho, A.V., Hopcroft, J.E., Ullman, J.D., „The Design and Analysis of Computer Algorithms“, Addison-Wesley, Reading, Mass., 1974 [Ba] Genomdatenbank, Bakterien. ftp://ftp.ncbi.nih.gov/genomes/Bacteria/ [BBEHCS 84] Blumer, A., Blumer, J., Ehrenfeucht, A., Haussler, D., Chen, M.T., Seiferas, J. „The Smallest Automaton Secognizing The Subwords of a Text“, in Theoretical Computer Science 40, 1985, 31-55. [Br 09] Brill, O., „Charakterisierung von DNA-Sequenzen mit Suffix-Bäumen“, Diplomarbeit, http://www.psue.uni-hannover.de/forschung/abschlussarbeiten/brill.pdf [CR 86] Crochemore, M., „Transducers and repetitions“, in Theoretical Computer Science 45, 1986, 63-86 [C-R 92] Crochemore, M., Czumay, A., Gasieniec, L., Jarominek, S., Lecroq, T., Plandowski, W., Rytter, W., „Speeding up two string-matching algorithms“, in 9th Annual Symposium on Theoreitcal Aspects of Computer Science, 1992, 589-600 [FM 96] Farach, M., Muthukrishnan, S., „Optimal Logarithmic Time Randomized Suffix Tree Construction“, in Proceedings of the 23rd International Colloquium on Automata, Languages and Programming, Vol.1099, 1996, 550-561 [Gu] Projekt Gutenberg. Online Buch Katalog. http://www.gutenberg.org/ [KA 03] Ko, P., Aluru, S., „Space Efficient Linear Time Construction of Suffix Arrays“, in Lecture Notes in Computer Science, Vol. 2676, 2003, 200-210 [MM 93] Manber, U., Myers, G., „Suffix Array: A New Method for On-line String Searches“, in SIAM Journal on Computing, Vol. 22, 1993, 935-948. [McC 75] McCreight, E.M., „A space-economical suffix tree construction algorithm“, XEROX Palo Alto Research Center, 1975 [Op 05] Opitz, H., „Suffix-Bäume und verwandte Datenstrukturen“, Diplomarbeit, http://www.psue.uni-hannover.de/forschung/abschlussarbeiten/ opitz.pdf, 2005 71 [Uk 92] Ukkonen, E., „Constructing suffix trees on-line in linear time“, in IFIP, 1992, 484-492. [Uk 95] Ukkonen, E., „On-line construction of suffix trees “in Algorithmica, 1995, 14: 249-260. [We 73] Weiner, P., „Linear pattern matching algorithms“in Proc. 14th IEEE Annual Symposium on Switching and Automata Theory, Institute of Electrical Electronics Engineering, New York, 1973: 1-11 [Wi] Wikisource. Freie Bibliothek http://en.wikisource.org/wiki/ [ZK 09] Parchmann, R., Skript zur Vorlesung „Zeichenkettensuche “, Universität Hannover, gehalten im Wintersemester 2008/2009 72 für Online-Veröffentlichungen.