Gerichtete azyklische Graphen und Suffix

Werbung
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.
Herunterladen