pdf-Version

Werbung
Suffixbaumerstellung
Ronny Peine
Matr.-Nr: 2177913
Universität Hannover
Studienarbeit
Wintersemester 2005/2006
Unter Leitung von: Prof. Dr. R. Parchmann
12. April 2006
1
Inhaltsverzeichnis
1 Einführung
1.1 Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2 Die
2.1
2.2
2.3
2.4
2.5
2.6
Algorithmen
Überblick . . . . . . . . . . . . . . . . . . . . . .
Beispiel: Suffix-Zeiger . . . . . . . . . . . . . . . .
Der Algorithmus von McCreight . . . . . . . . . .
Schematische Darstellung McCreight-Algorithmus
Der Algorithmus von Ukkonen . . . . . . . . . . .
Schematische Darstellung Ukkonen-Algorithmus .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3
4
5
5
5
6
8
11
13
3 Die Datenstruktur Suffixbaum
14
3.1 Generischer Suffixbaum . . . . . . . . . . . . . . . . . . . . . . 14
3.2 Binärer Suffixbaum . . . . . . . . . . . . . . . . . . . . . . . . 16
4 Die Implementierung
17
5 Benchmarks und Laufzeitfaktorabschätzung
5.1 Die Benchmarkumgebung . . . . . . . . . .
5.2 Das Benchmarkverfahren . . . . . . . . . . .
5.3 Die Benchmarks . . . . . . . . . . . . . . . .
5.4 Die Benchmarkergebnisse . . . . . . . . . . .
5.5 Fazit . . . . . . . . . . . . . . . . . . . . . .
18
18
18
19
20
24
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6 Benutzung des Programms
25
6.1 Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
6.2 Makefile-Erläuterungen . . . . . . . . . . . . . . . . . . . . . . 26
2
1
Einführung
In dieser Studienarbeit geht es um die Implementierung von Algorithmen zur
Erstellung von Suffixbäumen. Dabei werden zwei effiziente Algorithmen vorgestellt. Desweiteren werden Datenstrukturen für den Suffixbaum dargelegt.
Zunächst klären wir erstmal, was ein Suffixbaum ist.
Ein Suffixbaum ist ein Baum, der alle Suffixe eines gegebenen Strings darstellt.
Genauer: sei Σ eine endliche, nicht leere Menge, dann nennen wir Σ ein Alphabet. Die Elemente von Σ nennen wir Terminale. Die Konkatenation von
Terminalen bezeichnen wir als Wörter, dabei ist das leere Wort, da es kein
Terminal enthält. Σ∗ ist dabei die Menge aller Wörter über dem Alphabet
Σ. Sei weiter w ∈ Σ∗ und u, v ∈ Σ∗ , so dass w = uv, dann nennen wir v ein
Suffix von w.
Um Suffixe eindeutig zu bezeichnen, verwenden wir die Notation sufi , mit
i ∈ N und i ≤ |w|, wobei w ein Wort über Σ und |w| die Länge des Wortes
w ist. Die Länge eines Wortes w ist dabei wie folgt definiert:
1. || = 0
2. |xw| = |w| + 1 für x ∈ Σ, w ∈ Σ∗
Dabei ist sufi das Suffix, dass mit dem i-ten Buchstaben des Wortes beginnt. Für i = 1 ergibt sich damit suf1 = w.
Kommen wir nun zur Definition von Bäumen. Ein Baum ist ein Tupel (V, E),
wobei V eine endliche, nicht leere Menge von Knoten und E eine endliche
Menge von gerichteten Kanten ist. Dabei ist eine Kante ein Tupel (u, v) mit
u, v ∈ V , was eine gerichtete Verbindung vom Knoten u zum Knoten v darstellt. Für jeden Baum gilt:
1. ∃! einen ausgezeichneten Knoten u ∈ V , so dass ∀v ∈ V : (v, u) ∈
/ E.
Diesen Knoten nennen wir die Wurzel (auch root genannt).
2. ∀v ∈ V, v ist nicht die Wurzel: ∃!u ∈ V mit (u, v) ∈ E. Jeder Knoten
außer der Wurzel besitzt also einen Elternknoten. u nennen wir dabei den
Elternknoten (auch parent genannt) und v einen Tochterknoten (auch child
genannt) von u.
3. ∀u, v ∈ V : Gibt es einen Pfad von u nach v, so gibt es keinen Pfad von v
nach u. Der Baum muss also azyklisch sein. Ein Pfad ist dabei eine endliche
Folge von Kanten (ui , vi ) ∈ E, i ∈ N, mit vi = ui+1 .
Die Knoten, die keine Kinder haben, nennen wir Blätter, alle anderen Knoten
nennen wir innere Knoten.
3
Ein Suffixbaum T zu einem präfixfreien Wort w ∈ Σ∗ ist nun ein Baum
für den gilt:
1. Jede Kante von T ist mit einem nicht leeren Teilwort von w beschriftet.
2. Für je zwei paarweise verschiedene Töchterknoten von einem Knoten u
beginnen die Kantenbeschriftungen mit einem verschiedenen Terminal.
3. x ist Suffix von w ⇔ es gibt ein Blatt v und einen Pfad von der Wurzel
zu v mit: die Konkatenation der Kantenbeschriftungen von der Wurzel nach
v ergibt das Suffix.
4. Jeder innere Knoten außer der Wurzel besitzt mindestens zwei Töchterknoten.
Ein Wort w ∈ Σ∗ ist präfixfrei genau dann, wenn es kein Suffix von w gibt,
dass Präfix eines anderen Suffixes von w ist.
1.1
Beispiel
Suffixbaum für den String abaab$.
4
2
2.1
Die Algorithmen
Überblick
In dieser Studienarbeit sind zwei Algorithmen implementiert worden. Beide Algorithmen erstellen einen Suffixbaum von einem gegebenen präfixfreien
Wort w in linearer Zeit, also in O(|w|). Es soll dabei der Faktor c für die
Laufzeit möglichst genau durch Messung abgeschätzt werden.
Aus [Op05] geht dabei hervor, dass Suffixbäume maximal 2 · |w| − 1 Knoten
haben. Durch Verwendung von Indizes statt echten Strings für die Kantenbeschriftung, erhält man somit auch insgesamt einen Speicherverbrauch von
O(|w|) für den Suffixbaum. Dies nennt man auch Edge-label Kompression.
Dabei speichert man den Anfangspunkt und Endpunkt eines Teilwortes in
dem Wort w, z.B. für baa in abaab$ ergibt sich [1,3], falls wir bei 0 anfangen
zu zählen. Somit ist erst eine Laufzeit von O(|w|) für die Erstellung eines
Suffixbaumes möglich. Um nun den Algorithmus von McCreight bzw. Ukkonen zu verstehen, benötigen wir noch den Begriff des Suffix-Zeigers.
Seien x ∈ Σ und α ∈ Σ∗ so, dass ein Knoten u in dem Suffixbaum für das
Wort xα und ein Knoten v für das Wort α existiert. Dann setzen wir einen
Zeiger von u nach v, diesen nennen wir Suffix-Zeiger. Damit wird es uns
möglich sein, eine gute Laufzeit für die beiden Algorithmen zu ermöglichen.
2.2
Beispiel: Suffix-Zeiger
5
2.3
Der Algorithmus von McCreight
Der Algorithmus von McCreight arbeitet sukzessiv, indem er beginnend von
i = 1 bis i = |w| die Suffixe sufi dem enstehenden Suffixbaum hinzufügt.
Dabei beginnt er mit dem Baum, der nur aus der Wurzel besteht und fügt
zuerst suf1 also das gesamte Wort w in den Baum ein. Um nun nicht jedes
Mal von der Wurzel aus zu dem Knoten zu navigieren, wo ein neuer Knoten
erzeugt werden soll, verwendet man die Suffix-Zeiger.
Um von sufi zu sufi+1 zu kommen, benötigen wir noch den Begriff von headi .
Dabei ist headi das längste Präfix von sufi , dass auch Präfix von sufj ist mit
j < i. Umgangssprachlich ist es also das längste Präfix von sufi , dass mit den
vorherigen Suffixen übereinstimmt. Dabei ist u ∈ Σ∗ ein Präfix von w, falls
es ein Wort v ∈ Σ∗ gibt mit w = uv. In [Op05] ist gezeigt, dass headi ohne
das erste Zeichen ein Präfix von headi+1 ist. Somit können wir ausgehend
vom Knoten der headi darstellt zum Knoten headi+1 gehen ohne für headi+1
vollständig von der Wurzel durch den Suffixbaum zu navigieren.
Wir gehen dabei vom Knoten u der headi darstellt zum Elternknoten, falls
headi nicht die Wurzel ist. Dabei merken wir uns die Kantenbeschriftung β
vom Elternknoten zu u. Vom Elternknoten aus folgen wir dann dem SuffixZeiger, falls der Elternknoten nicht die Wurzel ist, zu einem Knoten v. Von
dort aus müssen wir nur noch die Stelle für β finden, indem wir ein schnelles Navigieren verwenden. Dabei vergleichen wir nur das erste Zeichen einer
Kante. Falls dieses übereinstimmt mit dem ersten Zeichen von β so folgen
wir dieser Kante. Ist |β| > der Länge der Kantenbeschriftung, so verkürzen
wir β um so viele Zeichen beginnend vom ersten Zeichen von β, wie die
Kantenbeschriftung lang ist und beginnen die schnelle Navigation nun mit
dem verkürzten β ab dem gerade erreichten Knoten. Ist |β| = der Länge der
Kantenbeschriftung, so folgen wir der Kante zu dem jeweiligen Knoten und
enden dort. Ist |β| < der Länge der Kantenbeschriftung, so merken wir uns
die Stelle nach dem |β|-ten Zeichen der Kantenbeschriftung.
In dem Fall, dass headi der Wurzelknoten war, müssen wir leider langsam
durch den Baum beginnend bei der Wurzel navigieren und dabei jedes einzelne Zeichen von sufi+1 mit der Kantenbeschriftung vergleichen. Beim ersten
Unterschied bleiben wir an der Stelle stehen, egal ob es in einer Kante oder
einem Knoten ist.
Ist der Fall aufgetreten, dass der Elternknoten vom headi repräsentierenden
Knoten die Wurzel ist, dann müssen wir von β das erste Zeichen entfernen
6
und von der Wurzel aus diesmal mit dem schnelleren Navigieren das übrig
gebliebene β ohne den ersten Buchstaben im Baum finden.
Nun heißt es, das fehlende Stück von headi+1 noch zu finden, da headi ja
nur ein Präfix von headi+1 ist. Im Fall das headi die Wurzel war, befinden
wir uns bereits an der richtigen Stelle. Sind wir in einer Kante, so splitten
wir die Kante in zwei Teile, indem wir einen Knoten dazwischen einfügen,
dessen eingehende Kante den übereinstimmenden Teil mit unserem β erhält.
Die ausgehende Kante erhält dabei den Rest der Kantenbeschriftung. Dieser
neue Knoten repräsentiert nun headi+1 . Von diesem neuen Knoten aus erzeugen wir einen Tochterknoten, dessen Kantenbeschriftung des Restes vom
Suffix sufi+1 ohne headi+1 entspricht.
Befinden wir uns im Falle headi = root in einem Knoten statt in einer Kante
nach dem Navigieren, so ist dieser Knoten headi+1 und wir fügen nur noch
wie im Fall vorher sufi+1 ein.
In den übrigen Fällen, wo headi nicht der Wurzel entsprach, und wir in einem
Knoten endeten, müssen wir noch mit Hilfe der langsamen Navigation den
Rest von sufi+1 im Baum suchen. Bei der ersten Unterscheidung bleiben wir
stehen. Befinden wir uns in einer Kante, so müssen wir wie oben bereits beschrieben die Kante splitten. Der dabei entstehende Knoten ist dann headi+1 .
An diesem fügen wir einen neuen Tochterknoten mit dem Rest von sufi+1 an.
Enden wir hingegen in einem Knoten, so stellt dieser headi+1 dar und wir
fügen nur noch den Rest von sufi+1 ein.
In dem übrigen Fall, wo headi 6= root, und wir beim Navigieren in einer
Kante geendet sind, splitten wir die Kante. Der neu entstehende Knoten ist
dann headi+1 und wir müssen nur noch den Rest von sufi+1 einfügen.
Um auch weiterhin Suffix-Zeiger für die schnelle Navigation nutzen zu können,
wird im Fall headi 6= root ein Suffix-Zeiger von headi zum gefundenen bzw.
erzeugten Knoten nach der schnellen Navigation erzeugt.
Ist dies iterativ für alle Suffixe erfolgt, so ist der Suffixbaum erstellt. Die
Korrektheit dieses Verfahrens, sowie dass das schnelle Navigieren auch funktioniert und die Laufzeit O(|w|) ist, ergibt sich aus [Op05].
Nachfolgend eine schematische Darstellung der Fälle und Vorgehensweisen.
Dabei bedeutet β(i) das i-te Zeichen des Wortes β ∈ Σ∗ .
7
2.4
Schematische Darstellung McCreight-Algorithmus
1. Fall: headi = root, Navigation landet in einem Knoten
2. Fall: headi = root, Navigation landet in einer Kante
8
3. Fall: headi 6= root, Elternknoten = root, schnelle Navigation landet in
einem Knoten, langsame Navigation endet in einem Knoten
4. Fall: headi 6= root, Elternknoten = root, schnelle Navigation landet in
einem Knoten, langsame Navigation endet an einer Kante
9
5. Fall: headi 6= root, Elternknoten = root, schnelle Navigation landet in
einer Kante
6. Fall: headi 6= root, Elternknoten 6= root, schnelle Navigation landet in
einem Knoten, langsame Navigation endet in einem Knoten
10
Die Fälle headi 6= root, Elternknoten 6= root, wobei die schnelle bzw.
langsame Navigation in einer Kante endet, sind analog zu den Fällen 4-5,
nur dass dabei dem Suffix-Zeiger gefolgt wird, wie im Fall 6 beschrieben.
2.5
Der Algorithmus von Ukkonen
Der Algorithmus von Ukkonen gehört zu den sogenannten Online-Algorithmen,
da er den Suffixbaum erstellen kann, während der Text übertragen wird. Dabei baut er den Suffixbaum sukzessiv durch die Suffixe der Präfixe des Textes
auf. Also z.B. für abaab$, die Suffixe für die Präfixe a, ab, aba, abaa, abaab
und abaab$. Diese Präfixe bilden dabei die Phasen des Algorithmus, genauer: in phasei werden die Suffixe von prefi in den Suffixbaum eingefügt. Dabei
ist prefi das Präfix des Textes der Länge i. Die Suffixe in einer Phase bilden
dabei die Extensionen, also in prefi wird das Suffix sufj , j ≤ i, in extensionj
eingefügt. Dabei können 3 Situationen auftreten, die als Regeln bezeichnet
werden.
Regel 1: Bei der Suche nach sufj in phasei+1 endet man in einem Blatt und
fügt das i + 1-te Zeichen des Textes ein.
Regel 2: Bei der Suche nach sufj in phasei+1 endet man in einem inneren
Knoten oder einer Kante und fügt einen weiteren Knoten für das i + 1-te
Zeichen des Textes ein.
Regel 3: sufj in phasei+1 ist vollständig mit dem i + 1-ten Zeichen des Textes
bereits im Suffixbaum enthalten.
Beim Aufbau des Suffixbaums handelt es sich dabei in jeder Phase um einen
impliziten Suffixbaum, da Suffixe auch mitten im Baum und nicht an einem
Blatt enden können. Durch hinzufügen eines letzten Zeichens, das nicht im
Text vorkommt, wird der Text präfixfrei, wodurch wieder jedes Suffix in einem Blatt endet.
Um nun eine Laufzeit von O(|w|) zu erhalten, müssen einige Tricks angewandt werden. Es gilt, wenn einmal in extensionj in phasei Regel 1 in Kraft
trat, so wird sie auch in allen folgenden Phasen k, k > i, in extensionj auftreten. Das heißt statt jedes mal dieses Suffix um einen Buchstaben zu erweitern,
fügt man es gleich vollständig ein. Da Indizes verwendet werden, bleibt die
online Eigenschaft erhalten, wenn man weiß, wie lang der Text ist. Bei der
schnellen Navigation mittels Suffix-Zeigern müssen dabei nur die Buchstaben
wk , k > i, in phasei+1 ignoriert werden. Damit kann man dann alle extensionj
in den folgenden Phasen ignorieren.
11
Der nächste Trick wird bei Regel 3 genutzt. Wenn einmal Regel 3 in extensionj
in phasei auftritt, so tritt diese Regel in allen extensionk , j < k ≤ i auf und
es kann somit direkt in phasei+1 weitergemacht werden.
Der letzte Trick bezieht sich auf Regel 2. Wenn einmal Regel 2 eintritt, so
wird dabei ein neues Blatt für sufj in extensionj in phasei erzeugt, was in
den nachfolgenden Phasen immer Regel 1 impliziert bei extensionj . Man fügt
also bei dem Blatt ebenfalls gleich das gesamte Suffix des Textes w ein und
überspringt dann diese Extension in den folgenden Phasen.
Desweiteren werden noch andere Tricks genutzt.
Es gilt: Regel 1 tritt vor Regel 2 auf für alle Extensionen in einer Phase. Genauso gilt: Regel 2 tritt vor Regel 3 auf in allen Extensionen in einer Phase.
Somit merkt man sich, falls Regel 1 oder 2 auftritt in welcher Extension man
ist und beginnt in den folgenden Phasen mit der nachfolgenden Extension.
Falls dabei nicht Regel 3 zum Zuge kam, fängt man wieder in der Extension
bei der Wurzel an zu suchen. Nutzt dann aber in den nachfolgenden Extensionen die Suche mittels Suffix-Zeiger. Ist hingegen Regel 3 aufgetreten, so
verbleibt man an dem Knoten, und beginnt in der nachfolgenden Phase i + 1
in der selben Extension mit der Suche nach dem i + 1-ten Zeichen des Textes
ausgehend von dem Knoten. Dabei kann wieder Regel 3 auftreten oder aber
Regel 2 und man fügt schließlich das Suffix ein.
Insgesamt erhält man eine lineare Laufzeit O(|w|).
Zu den theoretischen Grundlagen, sowie Beweise für die Korrektheit, siehe
[Sp03].
Fängt man mit einem Baum an, der das erste Suffix des Textes enthält und
beginnt direkt in extension2 , so erspart man sich mit obigen Tricks, dass Regel
1 überhaupt eintritt. Es müssen somit nur noch Regel 2 und 3 abgearbeitet
werden, die nach obiger Beschreibung sehr schnell vollführt werden. Die Navigation erfolgt dabei analog zu dem Algorithmus von McCreight, man sucht
also beginnend von headi nach headi+1 , mit Ausnahme der ersten Extension
einer Phase ohne das vorher Regel 3 angewandt wurde, dort beginnt man immer an der Wurzel. Bei der Navigation ist auch hier immer der Suffix-Zeiger
zu setzen, um sie später nutzen zu können.
12
2.6
Schematische Darstellung Ukkonen-Algorithmus
13
3
Die Datenstruktur Suffixbaum
Für eine optimale Laufzeit und einen möglichst geringen Speicherverbrauch
ist eine möglichst effiziente Datenstruktur für den Suffixbaum notwendig.
Dabei hat sich herausgestellt, dass insbesondere der Speicherverbrauch das
Hauptproblem bei Suffixbäumen darstellt. Es werden hier zwei Datenstrukturen für den Suffixbaum dargestellt, sowie eine geschätzte Berechnung für den
Speicherverbrauch auf Basis des Worst-Case-Falls mit 2·|w|−1 Knoten (siehe
[Op05]) durchgeführt. Es ist dabei zu sagen, dass beide Datenstrukturen relativ weit vom Optimum entfernt sind. Eine sehr effiziente Implementierung ist
sehr aufwendig, da viele Tricks genutzt werden, um den Speicherverbrauch in
den Griff zu bekommen teilweise auch zu Lasten der Laufzeit. Hoch effiziente
Datenstrukturen benötigen nach heutigem Wissen ca. 8.5 Bytes pro Zeichen
Text im Durchschnitt und 12 Bytes pro Zeichen Text im Worst-Case-Fall,
indem sie redundante Informationen aus dem Suffixbaum entfernen (siehe
[Ku03]). Diese effizienten Datenstrukturen füllen teilweise Doktorarbeiten
und sind deshalb vom Aufwand zu hoch, um in einer Studienarbeit behandelt zu werden.
Deshalb beschränke ich mich auf die beiden implementierten Datenstrukturen: der generische Suffixbaum sowie der binäre Suffixbaum.
Diese stellen soweit einen brauchbaren Kompromiss zwischen Laufzeit und
Speicherverbrauch dar, da fast alle Zugriffsmethoden, die von den Algorithmen verwendet werden, eine Laufzeit von O(1) haben.
3.1
Generischer Suffixbaum
Der generische Suffixbaum stellt soweit die intuitivste Datenstruktur dar. Die
Datenstruktur des Suffixbaums habe ich dabei als Typ suffixtree definiert. Die
folgende Darstellung wird in C-Syntax stattfinden, um eine genaue Betrachtung zu ermöglichen.
suffixtree ist dabei von folgender Gestalt:
Listing 1: suffixtree
struct s u f f i x t r e e {
text t ∗ text ;
struct s u f f i x t r e e n o d e ∗ r o o t ;
};
text t ist dabei standardmäßig vom Typ char, kann aber z.B. für Unicode
über andere Datentypen definiert werden. suffixtreenode ist dabei der Datentyp für einen Knoten.
14
Diese haben folgenden Aufbau:
Listing 2: suffixtreenode
struct s u f f i x t r e e n o d e {
position t position ;
t e x t p o s i t i o n tpos ;
struct s u f f i x t r e e n o d e ∗ s u f f i x l i n k ;
struct s u f f i x t r e e n o d e ∗ p a r e n t ;
struct s u f f i x t r e e n o d e ∗ ∗ c h i l d r e n ;
};
position t ist dabei ein Integerdatentyp, der bei position die Nummer des
Knotens darstellt, also die Zahl i bei sufi falls vorhanden. textposition ist dabei eine struct die zwei Elemente vom Typ position t enthält (begintext und
endtext). Diese stellen die Indizes dar, die für die Edge-label Kompression
des Textes verwandt werden. suffixlink ist der Suffix-Zeiger eines Knotens.
parent verweist auf den Elternknoten des Knotens falls vorhanden. children
ist ein Array von Zeigern auf Töchterknoten falls vorhanden.
Man erkennt, dass diese Darstellung eine eins zu eins Abbildung eines beliebigen Baumes ermöglicht. Allerdings ist der Speicherverbrauch recht hoch,
da bei children einerseits ein Array von Zeigern allokiert werden muss, sowie
die Töchterknoten noch zusätzlich Speicher benötigen.
Nehmen wir an, Zeiger hätten eine Größe von 4 Byte genauso wie Integer.
Dann ergibt sich somit im Worst-Case-Fall mit 2 · |w| − 1 Knoten und der
Annahme von 2 Töchterknoten pro Knoten einen Speicherverbauch von:
Textgröße + Wurzelzeigergröße +|w|· Blattgröße +(|w| − 1)· innere Knotengröße = |w| + 4 + |w| · (4 + 2 · 4 + 4 + 4 + 4) + (|w| − 1) · (4 + 2 · 4 + 4 + 4 + 2 · 2 · 4)
32
Bytes
= 61 · |w| − 32 Bytes für den gesamten Text w. Dies macht 61 − |w|
pro Zeichen.
Die knapp 61 Bytes pro Zeichen sind natürlich weit vom Optimum entfernt,
so dass sich diese Datenstruktur nur für relativ kleine Texte eignet.
15
3.2
Binärer Suffixbaum
Als Alternative zum generischen Suffixbaum bietet sich der binäre Suffixbaum an, dabei werden nicht sämtliche Töchterknoten einem Knoten über
children zugewiesen, sondern jeder Knoten besitzt zwei Zeiger vom Typ suffixtreenode, wobei ein Zeiger aufs erste Kind zeigt (child ) und der zweite
auf den nächsten Geschwisterknoten (sibling). Somit kann jeder Baum durch
einen binären Baum äquivalent repräsentiert werden. Dadurch spart man sich
die extra Speicherallokierungen für den Zeiger auf die Töchterknoten-Zeiger.
Bei einer Annahme, wie oben, von 4 Byte für Zeiger- und Integerspeichergröße ergibt sich für den Worst-Case-Fall von 2 · |w| − 1 Knoten:
Textgröße + Wurzelzeigergröße +(2 · |w| − 1)· Knotengröße
= |w| + 4 + (2 · |w| − 1) · (4 + 2 · 4 + 4 + 4 + 4 + 4) = 57 · |w| − 24
24
Bytes pro Zeichen Text. Damit ergibt sich gegenüber dem
Dies macht 57− |w|
generischen Suffixbaum immerhin eine Ersparnis von 4 Bytes Pro Zeichen für
große Texte w.
16
4
Die Implementierung
Die Implementierung der Algorithmen sowie der Datenstrukturen erfolgte in
C. Aus Testgründen für die Datenstrukturen wurde auch das naive Verfahren
implementiert, da es sehr leicht implementierbar ist und damit sich schnell
Testfälle für die komplizierteren Algorithmen von McCreight bzw. Ukkonen
ergeben.
Dabei wurden die Datenstrukturen abstrakt implementiert, wodurch die Algorithmen nur durch Methoden auf die Datenstrukturen zugreifen können.
Dies erlaubt eine Implementierung der Algorithmen unabhängig von den Datenstrukturen, so dass ein einfacher Wechsel der Datenstrukturen möglich ist.
Dabei nehmen die Algorithmen an, dass eine Datenstruktur Suffixbaum (suffixtree im Code genannt) einmal den Text als Datentyp text t* enthält sowie
den Wurzelknoten root. Knoten sind dabei vom Typ suffixtreenode. Insbesondere können die Algorithmen über get-Methoden die Elternknoten, Töchterknoten sowie Suffix-Zeiger erhalten. Der in Kanten repräsentierte Text wird
dabei dem Knoten zugewiesen, zudem die Kante führt, und ist vom Typ
textposition. textposition ist dabei ein Tupel aus zwei Elementen vom Typ
position t, die jeweils ein Index in dem als Array aufgefassten Text darstellen.
Um eine andere Datenstruktur einzubinden, sollte das include für datastructures/suffixtree bin base.h in suffixtreealgos.c geändert werden. Dabei müssen
sämtliche Methoden und Datentypen, die in suffixtree bin.h, suffixtree bin.c
und suffixtree bin base.h in datastructures zu finden sind, implementiert bzw.
definiert werden. Damit ist ein recht unkomplizierter Wechsel der Datenstruktur möglich und somit eine Optimierung dieser denkbar ohne die Algorithmen
anpassen zu müssen.
Genauere Informationen zur Implementierung sind dem Quellcode zu entnehmen, sowie aus den generierten Dokumentationen von Doxygen (make
docs). Doxygen generiert dabei aus dem Quelltext sowie den Kommentaren eine recht gute, Javadoc sehr ähnliche Dokumentation über die Dateien,
Datentypen und Methoden.
17
5
5.1
Benchmarks und Laufzeitfaktorabschätzung
Die Benchmarkumgebung
Die Benchmarks wurden auf einem Gentoo Linux System durchgeführt. Dabei liefen nur der Linux Kernel, udev und syslog-ng, um möglichst Einflüsse
durch andere Programme zu minimieren.
Als Hardware standen folgende Komponenten zur Verfügung:
CPU: Athlon XP 2600+ (1.9 GHz)
RAM: 2x512MB DDR-SDRAM als Dual-Channel genutzt
Chipsatz: VIA KT880
Auf Softwareseite dürften folgende Komponenten von Interesse sein:
Kernel: Linux 2.6.15 (gentoo-sources-2.6.15-r1)
Lib-C: glibc-2.3.5 (glibc-2.3.5-r3)
binutils: binutils-2.16.1
Compiler: gcc-3.4.5 (gcc-3.4.5-r1)
Das Programm für die Benchmarks suffixtreealgos wurde mit den
Compileroptionen (CFLAGS) -s -ansi -pedantic -Wall -D FILE OFFSET BITS=64
-O3 -fomit-frame-pointer -funroll-loops -pipe übersetzt.
Die Benchmarks wurden mithilfe des Makefiles gestartet (make benchmark ).
Als Datenstruktur wurde der binäre Suffixbaum verwandt.
5.2
Das Benchmarkverfahren
Im Verzeichnis benchmark des Projektverzeichnisses liegen die einzelnen Benchmarks als txt-Datei. Diese beinhalten einen präfixfreien Text, für den der
Suffixbaum erstellt werden soll. Das benchmark-Verzeichnis enthält auch das
Script benchmarksuite zum starten des Benchmarks, was von make benchmark verwendet wird. Die Benchmarkergebnisse werden dabei auf den Standardausgabekanal stdout ausgegeben. Sämtliche Suffixbäume für die txtDateien werden einmal mit dem McCreight- und einmal mit dem UkkonenAlgorithmus erstellt. Beim Benchmarkvorgang werden dabei für jede txtDatei 11 Iterationen durchgeführt, wobei nur die letzten 10 in die Messungen
einfließen. Dies wird getan, damit das Betriebssystem bei der ersten Iteration den Hauptspeicher freiräumt und dabei andere Prozesse in den Swap
verdrängt, da dies sonst die Messungen stark verfälschen würde. Danach
wird die Zeit für die übrigen 10 Iterationen sekundengenau gemessen und
durch 10 geteilt, um das arithmetische Mittel für eine Iteration zu erhalten.
18
Dies dient lediglich der Minimierung von Schwankungen und Rauschen des
Messprozesses.
5.3
Die Benchmarks
Es gibt drei Typen von Benchmarks im benchmark-Verzeichnis.
Einmal ist dies der xMio-a.txt-Test, dabei steht x für eine natürliche Zahl
zwischen 1 und 11. Es wird dabei ein präfixfreier Text verwandt, der nur
aus x Millionen a’s besteht und einem $ am Ende, um die Präfixfreiheit
zu garantieren. Diese Tests stellen den Worst-Case-Fall dar, was die Anzahl
an Knoten betrifft. Ebenfalls werden anhand dieser Tests die Laufzeitfaktorabschätzung durchgeführt. Aufgrund ihres kleinen Alphabets (es enthält
nur das a) werden sie sehr schnell vom McCreight- und Ukkonen-Algorithmus
durchgeführt, da hier die Suffix-Zeiger optimal zur Geltung kommen.
Der zweite Benchmarktyp ist ein aus zufälligen Zeichen generierter präfixfreier Text. Er wurde durch Auslesen aus /dev/urandom erzeugt und mithilfe
von uuencode in lesbare Zeichen umgewandelt. Durch ein $ am Ende des
Textes erlangt er seine Präfixfreiheit, da sämtliche Vorkommen von $ vorher durch ein Leerzeichen ersetzt wurden. Der Test ist wie auch der xMioa.txt-Test nach Textlänge aufgebaut und befindet sich in den Dateien xMioRandom.txt. Dieser Benchmarktyp zeichnet sich durch ein recht großes Alphabet aus, in dem er faßt die komplette Bandbreite eines char ausnutzt.
Außerdem sind die Zeichen möglichst gleich verteilt über dem Text durch
Nutzung von /dev/urandom. Dies minimiert die Nutzungsmöglichkeit von
Suffix-Zeigern, da viele vollkommen verschiedene head für die Suffixe existieren, die viele Verzweigungen bei den Knoten forcieren. Der Baum ist somit
relativ flach und breit. Auch dieser Benchmark wird für die
Laufzeitfaktorabschätzung herangezogen.
Als letzter Benchmarktyp kommt ein realistischer Fall zur Geltung, nämlich
ein Fall aus der Praxis. Es wird ein Suffixbaum aus der DNA von Chromosom
12 von Saccharomyces cerevisiae erstellt. Da Suffixbäume insbesondere in der
Bio-Informatik genutzt werden, um z.B. DNA-Vergleiche durchzuführen, gibt
dieser Test eine realistische Möglichkeit an, die Laufzeit in solchen Bereichen
festzustellen. Der Text zeichnet sich durch ein relativ kleines Alphabet aus
(nur A,G,C,T) und enthält ein $ am Ende des Textes, um auch hier die
Präfixfreiheit zu gewährleisten. Der Text ist dabei 1078175 Zeichen lang.
Auch hier spielen Suffix-Zeiger in der Regel ihre Stärken aus, was zu relativ
geringen Laufzeiten führt.
19
5.4
Die Benchmarkergebnisse
Zunächst erst einmal die Messergebnisse für den xMio-a.txt-Test.
Es ist zu erkennen, dass die Laufzeit für 1 Millionen a’s lediglich 0.5 Sekunden
benötigt. Für jede weitere Million a’s werden ca. zusätzliche 0.5 Sekunden in
etwa benötigt. Es ergibt sich somit ein Laufzeitfaktor von 0.5 · 10−6 Sekunden, sprich die Laufzeit beträgt 0.5 · 10−6 · |w| Sekunden für den McCreightAlgorithmus. Hierbei ist sehr gut die lineare Laufzeit des Algorithmus zu
erkennen. Da dieser Benchmarktyp jedoch stark durch Suffix-Zeiger profitiert, ist diese Abschätzung sehr optimistisch.
20
Der Ukkonen-Algorithmus zeigt hierbei eine fast identische Laufzeit, was
nachfolgende Messergebnisse zeigen.
Auch hierbei ist ein Laufzeitfaktor von 0.5 · 10−6 zu erkennen, der zur selben
Laufzeit von 0.5 · 10−6 · |w| wie beim McCreight-Algortihmus führt. Ebenfalls
profitiert dieser Algorithmus sehr stark von Suffix-Zeigern, so dass sich diese
optimistische Laufzeit einstellt.
21
Für den xMio-Random.txt-Test ergaben sich folgende Messwerte für den
McCreight-Algorithmus:
Hierbei zeigt sich die relativ gute Gleichverteilung der lesbaren Zeichen eines
char, indem die Laufzeit relativ schlecht ist, da Suffix-Zeiger hier weniger
stark genutzt werden können.
Es ergibt sich ein Laufzeitfaktor von ungefähr 12.5·10−6 Sekunden und somit
eine Laufzeit von 12.5 · 10−6 · |w| Sekunden. Diese Eingabedaten sorgen somit
für eine ungefähr 25 mal langsamere Laufzeit als beim xMio-a.txt-Test. Dieser
Fall kann so ziemlich als Worst-Case-Fall angesehen werden und dürfte in der
Praxis sehr selten auftreten.
22
Beim Ukkonen-Algorithmus zeigen sich fast die selben Messwerte, die
nur um wenige Zehntelsekunden von den Messergebnissen des McCreightAlgorithmus abweichen:
Auch hier wird ein Laufzeitfaktor von ungefähr 12.5 · 10−6 Sekunden ausgemacht, so dass sich die selbe Laufzeit von 12.5 · 10−6 · |w| Sekunden ergibt.
Eine Abschätzung des Laufzeitfaktors für den durchschnittlichen Fall könnte
das arithmetische Mittel von dem xMio-a.txt-Test und dem xMio-Random.txtTest darstellen, da beide Benchmarks den Best- bzw. Worst-Case-Fall betrachten. Es ergibt sich somit ein Laufzeitfaktor von:
0.5+12.5
· 10−6 = 6.5 · 10−6 Sekunden und damit eine Laufzeit von 6.5 · 10−6 · |w|
2
Sekunden.
23
Beim Saccharomyces cerevisiae DNA Benchmark ergab sich eine Laufzeit von 1.6 Sekunden beim McCreight-Algorithmus und 1.7 Sekunden beim
Ukkonen-Algorithmus. Beide haben also mit Blick auf die Messgenauigkeit
in etwa die selbe Laufzeit.
Es zeigt sich, dass das Erstellen eines Suffixbaumes für DNA-Text vergleichsweise effizient ist, da ein relativ kleines Alphabet vorhanden ist. Wenn man
diese Laufzeit ohne Berücksichtigung des Speicherverbrauchs und eventuelle
Laufzeitverzerrungen durch Speicherzugriffe hochrechnet auf den geschätzten
3 Milliarden Zeichen umfassenden Text der menschlichen DNA, so würde sich
3·109
· 1.6 Sekunden ≈ 4452 Sekunden
eine Laufzeit von 1078175
= 1 Stunde 14 Minuten 12 Sekunden ergeben.
Der Suffixbaum ist natürlich meist nur einmal aufzubauen und kann dann
für weitere Verfahren wie String Matching verwendet werden.
Somit wären die Verfahren in ihrer jetzigen Implementierung schon recht
vernünftig in der Praxis nutzbar. Lediglich der Speicherverbrauch müsste
durch effizientere Datenstrukturen gesenkt werden, was aber in der Regel zu
einer höheren Laufzeit führen würde.
5.5
Fazit
Es zeigt sich in den Benchmarks, dass der McCreight-Algorithmus genauso
schnell operiert wie der Ukkonen-Algorithmus. Beide haben eine lineare Laufzeit und es ergab sich ein Laufzeitfaktor von im Mittel 6.5 · 10−6 Sekunden
auf dem Testsystem.
24
6
Benutzung des Programms
Um die Algorithmen und Datenstrukturen testen zu können, wurden sie in
einem Programm suffixtreealgos eingebunden. Dies kann sehr leicht über die
Kommandozeile genutzt werden. Dabei können folgende Optionen genutzt
werden:
-h: Damit läßt sich die Hilfe des Programms anzeigen, die die einzelnen Optionen erklärt.
-i <DATEINAME>: Mit dieser Option übergibt man dem Programm die
Eingabedatei, die zum Auslesen des Textes verwendet wird, um den Suffixbaum auf dieser Basis zu erstellen.
Dabei ist darauf zu achten, dass jedes Byte als ein Zeichen verstanden wird
(im Fall text t = char = 1 Byte), somit auch newline-Zeichen am Ende des
Textes mitgelesen werden. Ebenfalls ist auf die Präfixfreiheit des Textes zu
achten, da sonst das naive Verfahren und der McCreight-Algorithmus versagen. Gute Beispiele befinden sich im test- und im benchmark -Verzeichnis.
-m <ALGORITHM>: Hiermit läßt sich das Verfahren für die
Suffixbaumerstellung bestimmen. Zur Auswahl stehen naive für das naive
Verfahren, sowie mccreight und ukkonen für die beiden anderen Algorithmen.
-b: Diese Option unterdrückt die Ausgabe, um bei Benchmarks lediglich die
Zeit für den Aufbau des Suffixbaumes sowie dessen Speicherfreigabe zu messen. Dadurch gibt es weniger Verzerrungen bei der Laufzeitmessung.
Das Programm geht folgendermaßen vor. Erst liest es die per -i Option übergebene Datei ein und erstellt den Suffixbaum mithilfe des mit -m spezifizierten Algorithmus. Danach gibt es den Suffixbaum als String kodiert aus, falls
nicht -b gesetzt ist. Die Ausgabe hat dabei folgendes Format:
( [Node: <KNOTENNUMMER>, Edgetext: <KANTENTEXT> (KIND 1)
... (KIND M) )
Dabei sind KIND 1 bis KIND M genauso kodiert mit
( [Node: <KNOTENNUMMER>, Edgetext: <KANTENTEXT>] ... ).
Die Darstellung ist somit eindeutig und unterliegt nicht den Nachteilen manch
anderer Darstellung mit Pfeilen, die bei großen Suffixbäumen aufgrund relativ kleiner Bildschirmbreiten schnell einen unschönen Zeichensalat fabrizieren. Desweiteren funktioniert sie für beliebig lange Texte und ist nach
einiger Eingewöhnung schnell in eine für Menschen besser lesbare graphische
25
Baumdarstellung konvertierbar.
6.1
Beispiel
suffixtreealgos -i test/test01.txt -m mccreight liest die Datei test01.txt im
Ordner test ausgehend vom aktuellen Ordner ein und erstellt den Suffixbaum aus diesem Text mithilfe des McCreight-Algorithmus. Bei Annahme,
dass dies im Projektordner dieser Studienarbeit geschehen ist, ergibt sich folgende Ausgabe:
Output of suffixtree as string:
( [ Node: <none>, Edgetext: <none> ] ( [ Node: <none>, Edgetext: a ]
( [ Node: <none>, Edgetext: b ] ( [ Node: 1, Edgetext: aab$ ] ) ( [ Node: 4,
Edgetext: $ ] ) ) ( [ Node: 3, Edgetext: ab$ ] ) ) ( [ Node: <none>, Edgetext:
b ] ( [ Node: 2, Edgetext: aab$ ] ) ( [ Node: 5, Edgetext: $ ] ) ) ( [ Node: 6,
Edgetext: $ ] ) )
Dies ist die Stringdarstellung des Suffixbaumes für den Text abaab$.
6.2
Makefile-Erläuterungen
Das Makefile befindet sich im Projektverzeichnis. Mithilfe dessen ist es leicht
das Programm suffixtreealgos zu compilieren, zu testen und zu benchmarken.
Durch Aufruf von make <OPTION> wird die gewünschte Aktion durchgeführt. Auf nicht GNU-Systemen ist eventuell statt make gmake aufzurufen. Falls keine Option übergeben wird, wird standardmäßig das Programm
suffixtreealgos compiliert.
Folgende Optionen stehen zur Auswahl:
help: Dies führt zur Ausgabe einer kleinen Hilfe, die alle möglichen Optionen
nennt.
suffixtreealgos: Mithilfe dieser Option wird das Programm suffixtreealgos compiliert.
docs: Durch diese Option wird das Generieren der Dokumentation gestartet.
Dabei erzeugt Doxygen eine HTML-Dokumentation mithilfe des Quelltextes und den vorgegebenen Einstellungen in doc/doxygen-conf. Die erzeugte
Dokumentation landet dabei in doc/html. Sie können durch öffnen der Datei index.html im doc/html-Verzeichnis mithilfe eines Browsers eingesehen
werden.
26
Desweiteren wird die Studienarbeit aus der tex-Datei suffixbaumerstellung.tex in doc/studienarbeit compiliert. Dies geschieht mithilfe von pdflatex.
Die so erzeugte PDF-Datei kann in doc/studienarbeit geöffnet und eingelesen
werden, z.B. mithilfe von Acrobat Reader.
test: Diese Option führt zum Ausführen der testsuite, die sich in dem testVerzeichnis befindet. Dabei werden 11 Tests von jedem implementierten Algorithmus (Naiv, McCreight, Ukkonen) durchgeführt. Für jede testxy.txt-Datei
wird dabei der Suffixbaum erstellt und mit resultxy.txt die Ausgabe verglichen. Bei Übereinstimmung gilt der Test als bestanden, bei Unterschieden in
der Ausgabe ist der Test fehlgeschlagen.
benchmark: Bei dieser Option wird die Benchmarksuite gestartet. Nähere
Informationen dazu befinden sich im Kapitel Benchmarks und Laufzeitfaktorabschätzung.
clean: Diese Option führt zum Aufräumen des Projektverzeichnisses. Dabei
werden alle generierten Dateien gelöscht bis auf die PDF-Datei der Studienarbeit.
Um eventuelle Anpassungen für systemspezifische Konfigurationen vorzunehmen, kann das Makefile editiert werden. Am wichtigsten dürfte hier die Wahl
des Compilers sein. Dieser kann durch die CC-Variable gesetzt werden. Diese Variable enthält dabei das auszuführende Programm, so wie man es in
der shell aufrufen würde. Die CFLAGS-Variable gibt dabei an welche Optionen an den Compiler zu übergeben sind. Die DOXYGEN-Variable legt
die auszuführende Datei für das Programm Doxygen fest. Ebenso legt die
PDFLATEX-Variable die auszuführende Datei für pdflatex fest. Falls Doxygen oder pdflatex nicht auf dem System vorhanden sind, können die entsprechenden Zeilen bei docs: auskommentiert werden. Dies kann mit dem Zeichen
# am Anfang der Zeile erfolgen.
Für weitere oder detailliertere Informationen empfiehlt sich [Make].
27
Literatur
[Op05] OPITZ, Heike: Suffix-Bäume und verwandte Datenstrukturen,
Diplomarbeit,
http://www-psue.informatik.uni-hannover.de/forschung/diplomarbeiten/opitz.pdf,
28.09.2005
[Sp03] SPERSCHNEIDER, Volker: Algorithmus von Ukkonen zur Konstruktion von Suffixbäumen, Skript zur Bioinformatik WS 03/04,
http://www.inf.uos.de/theo/veranst/bioinf0304/SuffixTreesUkkonen.pdf,
23.10.2003
[Ku03] GRIEGERICH, R.; KURTZ, S.; STOYE, J.: Efficient implementation of lazy suffix trees, Software - Practice and Experience,
http://www.zbh.uni-hamburg.de/staff/kurtz/papers/GieKurSto2003.pdf,
25.06.2003
[Make] SMITH, P.: GNU ’make’, Free Software Foundation, Inc.
http://www.gnu.org/software/make/manual/
03.04.2006
28
Herunterladen