# Suffixbaum

Ein Suffixbaum ist in der Informatik eine vielseitige Datenstruktur, die effiziente Lösungen zahlreicher Probleme im Bereich der Stringverarbeitung ermöglicht. Ein Suffixbaum speichert alle Suffixe (Endungen) einer Zeichenkette. Er kann in linearer Zeit mit linearem Speicherverbrauch aufgebaut werden und ermöglicht, einmal aufgebaut, das schnelle Durchführen vieler Operationen wie die Suche nach Wörtern oder Sätzen in längeren Texten.

## Definition

Ein Suffixbaum $T$ für einen String $S$ mit $n$ Symbolen ist ein gerichteter Baum, der die folgenden Bedingungen erfüllt:
* Der Baum hat $n$ Blätter
* Jede Kante ist mit einem Teilstring von $S$ beschriftet
* Jeder innere Knoten von $T$ (außer der Wurzel) hat mindestens zwei Kinder, deren Kantenbeschriftungen nie mit dem gleichen Symbol beginnen
* Für jedes Blatt $i$ in $T$ ergeben die Beschriftungen der Kanten auf dem Pfad von der Wurzel zu $i$ aneinander gehängt das Suffix von $S$, das an Index $i$ beginnt
* $T$ enthält alle Suffixe von $S$, wobei mehrfach auftretende Teilstrings nur einmal in $T$ enthalten sind (siehe Abbildung)
Ein solcher Baum kann nicht für Strings erzeugt werden, die ein Suffix enthalten, das auch ein Präfix von einem anderen Suffix ist. Beispielsweise ist das Suffix cd aus dem String abcdcd auch ein Präfix des Suffix cdcd. Um die vierte Regel nicht zu verletzen, kann in diesem Fall ein Endsymbol (typischerweise $) angehängt werden, das nicht im Alphabet des Strings vorkommt. Da dieses Zeichen nicht im Text vorkommen kann und das Ende jedes Suffixes ist, können alle Suffixe keine Präfixe mehr sein. Dadurch wird sichergestellt, dass es $n$ Blätter gibt, eins für jedes Suffix. Da alle internen Knoten (Wurzel ausgenommen) verzweigt sind, kann es höchstens $n-1$ solcher Knoten geben. Damit kann es mit Wurzel und Blättern insgesamt nicht mehr als $n+(n-1)+1=2n$ Knoten geben.
Die String-Tiefe $d(v)$ eines Knotens $v$ gibt an, wie viele Zeichen die kombinierten Kantenbeschriftungen auf dem Weg von der Wurzel zum Knoten haben. Für ein Blatt entspricht das der Länge des Suffixes, das es repräsentiert.

## Konstruktion

Der Baum besteht zu Anfang aus einem Wurzelknoten und einer Liste von (Zeigern auf) allen fortsetzbaren Stellen im Baum, das heißt zu Anfang aus lediglich einem Zeiger auf den Wurzelknoten.
Der Baum wird für ein schrittweise zu verlängerndes Wort aufgebaut. Für alle Knoten aus der Liste der fortsetzbaren Stellen wird eine Kante zu einem neuen Knoten gezogen. Diese Kante wird mit dem zuletzt an das Wort angefügten Buchstaben beschriftet. Dieser neue Knoten wird dann in eine Liste der fortsetzbaren Stellen für die nächste Iteration aufgenommen. Zuletzt wird immer auch der Startknoten mit in die neue Liste aufgenommen (da das leere Wort immer ein gültiges Suffix ist).
Existiert zu einer fortsetzbaren Stelle bereits eine Kante, deren Beschriftung dem zuletzt angefügten Buchstaben entspricht, so wird kein Knoten hinzugefügt, und stattdessen der bereits existierende Zielknoten in die Liste der fortsetzbaren Stellen aufgenommen.
Durch das Hinzufügen des Startknotens in jedem Schritt ist die Liste der fortsetzbaren Stellen nach $n$ Iterationen auch $n+1$ Elemente lang, was in quadratischer Laufzeit resultiert. Mithilfe des Suffixarrays und des LCP-Arrays ist es möglich, den Baum auch in linearer Zeit aufzubauen.
Dafür werden die Suffixe in der Reihenfolge des Suffixarrays $A$ eingefügt. Sei $T_{i}$ der partielle Suffixbaum, der die ersten $i$ Suffixe enthält und $H$ das LCP-Array.
Der Algorithmus beginnt mit $T_{0}$, also nur der Wurzel des Baums. Im Schritt $i+1$ wandern wir den rechtesten Pfad (also den Pfad von dem Blatt mit dem Label $A[i]$ bis zur Wurzel) von $T_{i}$ so lange herauf, bis wir den tiefsten Knoten $v$ mit der String-Tiefe $d(v)\leq H[i+1]$ finden. Wenn $d(v)=H[i+1]$, wird einfach ein neues Blatt $x$ als Kind von $v$ eingefügt. Dieses Blatt hat das Label $A[i+1]$. Damit erhalten wir $T_{i+1}$ und gehen zum nächsten Schritt. Wenn hingegen $d(v)<H[i+1]$ gilt, also das neue Suffix innerhalb einer Kante von $v$ zum rechtesten Kind $w$ abzweigt, muss die bestehende Kante $(v,w)$ aufgespalten werden. Dazu wird erst die Kante $(v,w)$ gelöscht. Dann wird ein neuer Knoten $y$ eingefügt, mit einer Kante $(v,y)$, die den Teil der Kantenbeschriftung bekommt, der direkt vor der Stelle liegt, an der aufgeteilt werden muss (formal: $S[A[i]+d(v)\dots A[i]+H[i+1]-1]$). Dann wird die Kante $(y,w)$ eingefügt, die den restlichen Teil der ursprünglichen Kantenbeschriftung erhält (formal: $S[A[i]+H[i+1]\dots A[i]+d(w)-1]$). Als Letztes wird ein neues Blatt $x$ (mit Label $A[i+1]$) und eine Kante $(y,x)$ eingefügt. Die Kante bekommt den Teil des neuen Suffixes $A[i+1]$ als Label, der bisher nicht von $S_{i}$ abgedeckt wird (formal: $S\left[A[i+1]+H[i+1]\right]$).
Obwohl das Heraufwandern auf dem rechten Pfad in einem einzigen Schritt $O(n)$ Zeit in Anspruch nehmen könnte, ist die Laufzeit dieses Algorithmus durch $O(n)$ insgesamt begrenzt, denn: Jeder im Schritt $i$ durchquerte Knoten (außer dem letzten) wird vom rechten Pfad entfernt und wird für alle nachfolgenden Schritte $j>i$ nicht mehr betrachtet. Es werden also insgesamt höchstens $2n$ Knoten durchlaufen.

## Anwendungen

Suffixbäume ermöglichen eine effiziente Lösung zahlreicher Probleme im Bereich der Stringverarbeitung. Eine verbreitete Anwendung besteht in der Implementierung von String-Matching-Algorithmen in Suchmaschinen aufgrund der geringen Laufzeitkomplexität.
Für die Fuzzy-String-Suche ermöglicht ein Suffixbaum effiziente Verfahren, wie beim Vergleich mit wildcards oder k-mismatch oder in Form einer Beschleunigung der Ermittlung der Levenshtein-Distanz über hybrides dynamic programming.
Weitere Anwendungsgebiete sind in der Bioinformatik, u. a. in der DNA-Sequenzanalyse, und in der Datenkompression, zur Identifizierung von sich wiederholenden Daten, zu finden.

### Suchen in Suffixbäumen

Ein Suchtext $P[1,m]$ kann im Suffixbaum $T$ gefunden werden, indem der Baum von der Wurzel an Buchstabe für Buchstabe heruntergewandert wird. Dabei werden maximal $m$ Zeichen verglichen. Wenn an jedem Knoten die ausgehenden Kanten lexikografisch sortiert sind, kann die richtige Kante mit binärer Suche in $O(\log \sigma )$ Zeit gefunden werden, wobei $\sigma$ die Anzahl der ausgehenden Kanten ist. Dadurch ergibt sich eine Laufzeit von $O(m\log \sigma )$. Durch gewichtete binäre Suche kann diese Lautzeit auf $O(m+\log \sigma )$ verbessert werden, wobei das Gewicht der Kanten durch die Anzahl der Blätter, die durch diese Kante erreicht werden können, bestimmt wird.
Um herauszufinden, wie oft der Suchtext $P$ im String $S$ vorkommt, wird $T$ so lange traversiert, bis $P$ komplett eingelesen wurde. Dies bringt uns zu einem Knoten $v$ oder einer eingehenden Kante zu Knoten $v$. Die Anzahl der Blätter im Subbaum $T_{v}$, mit $v$ als Wurzel, gibt an, wie oft $P$ in $S$ vorkommt. Wenn diese Information in jedem Knoten mitgespeichert wird, kann dies in konstanter Zeit geschehen.
Um die Stellen zu finden, an denen der Suchtext $P$ vorkommt, wird wieder Knoten $v$ gesucht und dann für alle Blätter von $T_{v}$ die Stelle im Text ausgegeben, die sie repräsentieren.

## Geschichte

Das Konzept wurde erstmals von Weiner (1973) eingeführt. Anstelle des Suffixes $S[i\dots n]$ speicherte Weiner in seinem Trie den Präfixbezeichner für jede Position, d. h. die kürzeste Zeichenkette, die bei $i$ beginnt und nur einmal in $S$ vorkommt. Sein Algorithmus D nimmt einen unkomprimierten Trie für $S[k+1\dots n]$ und erweitert ihn zu einem Trie für $S[k\dots n]$. Auf diese Weise kann, ausgehend vom trivialen Trie für $S[n\dots n]$, ein Trie für $S[1\dots n]$ durch $n-1$ aufeinanderfolgende Aufrufe von Algorithmus D aufgebaut werden; die Gesamtlaufzeit beträgt jedoch $O(n^{2})$. Weiners Algorithmus B benutzt mehrere Hilfsdatenstrukturen, um eine Gesamtlaufzeit zu erreichen, die linear zur Größe des konstruierten Trie ist. Letztere kann immer noch aus $O(n^{2})$ Knoten bestehen, z. B. für $S=a^{n}b^{n}a^{n}b^{n}\$$. Weiners Algorithmus C verwendet schließlich komprimierte Tries, um eine lineare Gesamtspeichergröße und Laufzeit zu erreichen. Das Lehrbuch Aho, Hopcroft & Ullman (1974, Kap. 9.5) gab Weiners Ergebnisse in vereinfachter und eleganterer Form wieder und führte den Begriff Positionsbaum ein.
McCreight (1976) war der Erste, der einen (komprimierten) Trie aller Suffixe von S erstellte. Obwohl das bei $i$ beginnende Suffix in der Regel länger ist als der Präfixbezeichner, unterscheiden sich ihre Pfaddarstellungen in einem komprimierten Trie nicht in der Größe. Andererseits konnte McCreight auf die meisten der Weiner'schen Hilfsdatenstrukturen verzichten; es blieben nur die Suffix-Links übrig.
Ukkonen (1995) vereinfachte die Konstruktion weiter und lieferte die erste Online-Konstruktion von Suffixbäumen, die heute als Ukkonen-Algorithmus bekannt ist, mit einer Laufzeit, die den damals schnellsten Algorithmen entspricht. Diese Algorithmen laufen alle in linearer Zeit für ein Alphabet konstanter Größe und haben sonst im schlimmsten Fall eine Laufzeit von $O(n\log n)$.
Farach (1997) gab den ersten Algorithmus zur Konstruktion von Suffixbäumen an, der für alle Alphabete optimal ist. Insbesondere ist dies der erste Algorithmus mit linearer Zeit für Zeichenketten, die aus einem Alphabet von ganzen Zahlen in einem polynomialen Bereich gezogen werden. Farachs Algorithmus wurde zur Grundlage für neue Algorithmen zur Konstruktion von Suffixbäumen und Suffixarrays.