Suffixarray
# Suffixarray

Ein Suffixarray ist in der Informatik ein Array, das die Suffixe einer Zeichenkette in lexikographischer Reihenfolge angibt.

## Beispiel

Betrachte den String $S$=abracadabra der Länge 11. Da auch der leere String ein gültiges Suffix ist, fügen wir an das Ende von $S$ den Endmarker $ hinzu, um den leeren String bzw. das Ende von $S$ ausdrücken zu können. Der String mit den zugehörigen Positionen seiner Zeichen lautet also:

| i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| - | - | - | - | - | - | - | - | - | - | - | - | - |
| $S[i]$ | a | b | r | a | c | a | d | a | b | r | a | $ |

Dieser String hat zwölf Suffixe, die auch durch ihre Startposition i in $S$ beschrieben werden können:

| Suffix | i |
| - | - |
| abracadabra$ | 1 |
| bracadabra$ | 2 |
| racadabra$ | 3 |
| acadabra$ | 4 |
| cadabra$ | 5 |
| adabra$ | 6 |
| dabra$ | 7 |
| abra$ | 8 |
| bra$ | 9 |
| ra$ | 10 |
| a$ | 11 |
| $ | 12 |

Der leere String ist lexikografisch kleiner als jedes Suffix von $S$. Daher ist der Endmarker $ per Konvention ebenfalls lexikografisch kleiner als jedes andere Zeichen im String. Somit können alle Suffixe lexikografisch sortiert werden:

| Suffix | i |
| - | - |
| $ | 12 |
| a$ | 11 |
| abra$ | 8 |
| abracadabra$ | 1 |
| acadabra$ | 4 |
| adabra$ | 6 |
| bra$ | 9 |
| bracadabra$ | 2 |
| cadabra$ | 5 |
| dabra$ | 7 |
| ra$ | 10 |
| racadabra$ | 3 |

Als Array dargestellt ergibt sich {$, a$, abra$, …}.

Wenn der Originalstring bekannt ist, kann jedes Suffix platzsparend durch den Index seines ersten Zeichens spezifiziert werden. Das Suffixarray ist ein Array dieser Indizes in lexikographischer Reihenfolge. Für den String abracadabra$ lautet das Suffixarray somit $A$={12,11,8,1,4,6,9,2,5,7,10,3}: Das Suffix „a“ bzw. „a$“ beginnt am elften Zeichen, „abra“ beim achten und so weiter.

Der Endmarker $ ist nützlich, wenn mehrere Strings kombiniert werden. Beispielsweise ist in $S'$=abracadabra$mississippi$ sofort klar, dass der Text aus den beiden Strings abracadabra und mississippi besteht. Wenn nur ein String betrachtet wird, kann dieses Suffix übergangen werden: Da der leere String lexikographisch immer vor allen anderen sortiert wird, geht in diesem Fall keine Information verloren.

## Algorithmen

Es gibt eine Vielzahl an Algorithmen, um aus einem gegebenen Text der Länge n ein Suffixarray zu konstruieren. Die dabei verwendeten Methoden zur Suffix-Sortierung lassen sich grob in drei Klassen einteilen: Iterative, Rekursive und Induzierte Sortierung.

### Iterative Teilsortierung

Der einfachste Weg besteht darin, einen effizienten vergleichsbasierten Sortieralgorithmus zu verwenden, der höchstens $O(n\log n)$ Vergleiche benötigt. Da der Vergleich zweier Suffixe $O(n)$ Zeit benötigt, beträgt die komplette Laufzeit dieses Verfahrens $O(n^{2}\log n)$. Weiter entwickelte Algorithmen verbessern dies auf $O(n\log n)$, indem sie die Ergebnisse von Teilvergleichen ausnutzen und somit redundante Vergleiche vermeiden.

Dazu wird zunächst nur der erste Buchstabe jedes Suffixes betrachtet und daraus ein vorläufiger Suffixarray aufgebaut, der Suffixe mit gleichem Anfangsbuchstaben noch nicht untereinander sortiert hat. Suffixe mit unterschiedlichen Anfangsbuchstaben sind aber bereits in der richtigen Reihenfolge enthalten. In einem zweiten Schritt wird jede Gruppe von Suffixen mit gleichem Anfangsbuchstaben so sortiert, dass sie bezüglich der ersten beiden Anfangsbuchstaben korrekt sortiert sind. Der dritte Schritt sortiert alle Suffixgruppen mit zwei gleichen Anfangsbuchstaben so, dass sie bezüglich der ersten vier richtig sortiert sind, der vierte Schritt so, dass sie bezüglich der ersten acht richtig sortiert sind, und so weiter. Nach $\log _{2}n$ Schritten ist jedes Suffix vollständig richtig sortiert und der Suffixarray fertig aufgebaut. Jeder Teilschritt ist in Zeit $O(n)$ möglich, so dass sich die Gesamtlaufzeit $O(n\log n)$ ergibt.

Zu dieser Klasse gehören der klassische Suffixarray-Algorithmus von Manber und Myers sowie der in der Praxis deutlich effizientere Algorithmus von Larsson und Sadakane.

### Rekursive Algorithmen

Diese Algorithmenklasse wird erst seit 2003 erforscht. Der Zeichen des Strings $S$ werden dazu in zwei Zeichenketten x und y aufgeteilt. Anschließend wird der Algorithmus rekursiv auf x aufgerufen. Mit dem dadurch berechneten Suffixarray von x kann der Suffixarray von y effizient berechnet („induziert“) werden. Da die Aufteilung in x und y bekannt ist, kann daraus der Suffixarray von $S$ abgelesen werden.

Durch geschickte Wahl von x und y haben die meisten dieser Algorithmen eine Laufzeit von $O(n)$. Da Rekursion in der Praxis teuer ist, sind sie jedoch nicht immer zu bevorzugen.

### Induzierte Sortierung

Auch hier wird mit einem bereits berechneten Suffixarray einer Zeichenkette x der Suffixarray einer Zeichenkette y effizient berechnet (induziert). Anstelle einer rekursiven Arbeitsweise kann beispielsweise der String $S$ mehrfach in verschiedenen Richtungen durchlaufen werden. Dabei werden die Zeichen in x bzw. y klassifiziert, teilweise sortiert und in einem späteren Schritt auf Basis anderer Teilergebnisse weitersortiert. Die Algorithmen dieser Klasse sind sehr divers. Fast alle haben jedoch gemeinsam, dass sie trotz einer häufig schlechten Worst Case-Laufzeit von $O(n^{2}\log n)$ in der Praxis meist schneller als rekursive Algorithmen sind. Auch benötigen sie im Allgemeinen deutlich weniger Speicherplatz als andere Algorithmen.

Der derzeit schnellste Algorithmus dieser Klasse ist der Suffix-Array-Induced-Sorting-Algorithmus (kurz SAIS) von Nong, Zhang und Chan. Er benötigt lediglich eine Laufzeit in $O(n)$. Der SAIS-Algorithmus erweist sich auch in der Praxis als sehr schnell, wenn bei der Implementierung verschiedene Effekte wie z. B. Cache-Misses berücksichtigt werden.

## Anwendungen

Nach der Konstruktion kann das Suffixarray als Index des Texts verwendet werden, um nachfolgende Operationen effizient durchzuführen. Zu diesen Operationen zählen unter anderem Suchanfragen und Kompressionsverfahren.

### Exakte Suchanfragen (String Matching)

Eine exakte Suchanfrage besteht aus einem Suchmuster $P$, das in einem Text $T$ gesucht werden soll. Eine Textstelle zählt nur dann als exaktes Vorkommen von $P$, wenn jedes Zeichen der Textstelle mit $P$ übereinstimmt. Damit unterscheiden sich diese Anfragen von den nicht exakten Anfragen, bei denen eine festgelegte Anzahl an abweichenden Zeichen erlaubt ist. Es wird bei den exakten Suchanfragen zwischen Entscheidungsanfragen ("Kommt $P$ in $T$ vor?"), Anzahlanfragen ("Wie oft kommt $P$ in $T$ vor?") und Aufzählungsanfragen ("An welchen Stellen kommt $P$ in $T$ vor?") unterschieden, wobei Entscheidungsanfragen ggf. mithilfe von Anzahlanfragen und Anzahlanfragen mit Aufzählungsanfragen beantwortet werden können.

Um die Anzahl der exakten Vorkommen von $P$ zu bestimmen, müssen im Suffixarray zu $T$ alle Suffixe gesucht werden, die mit $P$ beginnen. Da das Suffixarray sortiert ist, liegen diese Suffixe alle direkt hintereinander und bilden einen Block. Daher reicht es aus, den lexikographisch kleinsten und den lexikographisch größten Suffix zu bestimmen, die mit $P$ beginnen. Mithilfe der binären Suche können diese jeweils in $O(m\cdot \log n)$ gefunden werden, wobei $m$ im Folgenden für die Länge von $P$ und $n$ für die Länge von $T$ steht. Die Anzahl der Vorkommen von $P$ ist dann gleich der Anzahl der Suffixe, die in diesem Block liegen. Damit kann die Anzahl der Vorkommen insgesamt in $O(m\cdot \log n)$ bestimmt werden. Wenn zusätzlich die Ausgabe der Anfangspositionen aller exakten Vorkommen gefordert ist, müssen stattdessen die Werte des Suffixarrays in dem Block zurückgegeben werden. Die Laufzeit hierfür beträgt $O(m\cdot \log n+\vert O_{P}\vert )$, wobei $\vert O_{P}\vert$ für die Anzahl der Vorkommen von $P$ in $T$ steht.

Verfeinerte Verfahren können unter Zuhilfenahme von weiteren Datenstrukturen bessere Laufzeiten erzielen. Wenn das sogenannte LCP-Array (englisch longest common prefix) zu $T$ bestimmt und eine geeignete RMQ-Datenstruktur (englisch range minimum query) zu dem LCP-Array konstruiert wurde, können Entscheidungs- sowie Anzahlanfragen in $O(m)$ und Aufzählungsanfragen in $O(m+\vert O_{P}\vert )$ beantwortet werden.

### Konstruktion eines Suffixbaums

Das Suffixarray eines Texts $T$ wird häufig als Zwischenschritt benutzt, um den zugehörigen Suffixbaum in Linearzeit zu konstruieren. Der Suffixbaum kann anschließend ebenfalls als Index genutzt werden, um Suchanfragen zu beantworten.

### Kompression

Verschiedene Kompressionsverfahren können unter Einsatz des Suffixarrays effizient umgesetzt werden. So kann die Faktorisierung der LZ77-Komprimierung in Linearzeit implementiert werden. Darüber hinaus kann man aus einem bestehenden Suffixarray mit sehr geringem Aufwand die Burrows-Wheeler-Transformation des Textes errechnen. Dazu wird nacheinander für jedes Suffix des Suffixarrays das Zeichen bestimmt, das im Text genau eine Position vor dem Suffix steht, und in einem Array abgelegt. Das resultierende Array ist dann identisch zur Burrows-Wheeler-Transformation des Textes und kann beispielsweise für das bzip2-Kompressionsverfahren verwendet werden.

## Geschichte

Suffixarrays wurden ursprünglich 1990 von Gene Myers und Udi Manber entwickelt, um den Speicherverbrauch im Vergleich zu Suffixbäumen zu reduzieren. Nachdem einige Jahre keine signifikanten Erkenntnisse auftraten, sind Suffixarrays seit ca. 2000 ein beliebtes Forschungsthema. Seitdem wurde eine Vielzahl von Konstruktionsalgorithmen entwickelt, die zahlreiche wünschenswerte Eigenschaften aufweisen.

Seit 1999 existieren Algorithmen, die in den meisten Anwendungsfällen schneller als die existierenden Linearzeitalgorithmen sind, schlimmstenfalls jedoch einen Zeitbedarf im Bereich $O(n\log n)$ bis $O(n^{2}\log n)$ haben. Die ersten garantierten Linearzeit-Algorithmen (d. h. solche mit Worst Case-Laufzeit $O(n)$) sind erst seit 2003 bekannt. Seit 2004 ist bekannt, dass Suffixarrays jedes Problem mit der gleichen Zeitkomplexität wie Suffixbäume lösen können. Spätestens seit diesem Zeitpunkt sind Suffixarrays daher für fast alle Suffix- und Stringsortierungsaufgaben das Mittel der Wahl.

Ab 2005 wurde neben der Konstruktion auch die effiziente Speicherung von Suffixarrays betrachtet. Neben reinen Suffixarrays können nun auch komprimierte Suffixarrays effizient konstruiert und verwendet werden. Auch für auf der Burrows-Wheeler-Transformation basierende komprimierte Volltext-Indizes sind sie nützlich.

