Proseminar "Pattern Matching" Grundlegende Such-Algorithmen Stephan Reichelt Wintersemester 2001/2002
2 1. Einführung Wozu muss man in Zeichenfolgen suchen? => Daten sind oft nicht in Datensätze (ähnlich einer Datenbank) zu pressen => werden häufig in eine (oft sehr lange) Folge von Zeichen geschrieben => Textverarbeitungssysteme => Strukturierung/Archivierung von Daten Für die Suche in Zeichenfolgen kommen im Bereich der grundlegenden Algorithmen der grobe Ansatz (Brute Force) sowie der Ansatz nach Boyer-Moore in Betracht. 2. Definitionen Zeichen sind elementare Bausteine, die für alle weiteren Betrachtungen zugrunde gelegt werden. Ein Alphabet ist eine Menge von Zeichen. Ein String x über einem Alphabet besteht aus einer Sequenz von Zeichen eines Alphabets. Beispiele: test = String über Alphabet {s, t, e, r, n} 1234 = kein String über Alphabet {1, 2, 3} Die Länge x eines Strings x ergibt sich aus der Anzahl Zeichen, aus denen dieser String besteht. Ein Substring von y ist eine konsekutive Folge von Zeichen aus dem String y. Ein Substring y = y i y i+1...y j wird als y(i,j) bezeichnet, wobei i j gilt. (für i>j gilt: x i x i-1...x j = x(i,j) ) Beispiele: x='einkleinertest' x(3,6) = nkle x(4,4) = k x(9,5) = eniel Daraus ergibt sich folgender Zusammenhang: y(i,j) y. Ein Suffix von x ist ein Substring der Form x(j, x ) mit j x. Ein Präfix von x ist ein Substring der Form x(1,j) mit j x. Als Mismatch zwischen zwei Zeichen ω 1 und ω 2 wird der Umstand ω 1 ω 2 bezeichnet. x t gibt das Zeichen in x an der Position t an. Eine äquivalente Darstellung dafür ist x[t]. Anmerkung: Für den Algorithmus von Boyer-Moore, der die Strings "von hinten" vergleicht, gilt diese Festlegung ebenso. Es sei angenommen, dass ein Zugriff auf x t für den Fall, dass t > m (m entspricht der Länge des Strings x, siehe "sonstige Vereinbarungen") ist, nicht zu einem Fehler führt und ein Zeichen zurückliefert, dass nicht zum Alphabet gehört. Ein Vergleich von x t mit einem Zeichen y i würde demnach immer fehlschlagen.
3 Sonstige Vereinbarungen für dieses Dokument Mit Pattern wird das zu suchende Muster x bezeichnet. Mit Text y wird die Zeichenfolge, in der das Pattern zu suchen ist, bezeichnet. Für die Länge von Pattern und Text wird folgendes vereinbart: x = m und y = n. Der Index in x, d.h. die aktuelle Position, wird mit j gekennzeichnet. Der Index in y sei i. Als String-Matching-Problem sei folgender Sachverhalt definiert: Gegeben seien 2 Strings x= x 1 x 2..x m-1 x m und y= y 1 y 2..y n-1 y n über einem Alphabet, wobei m n gilt (häufig gilt sogar m«n). Die Frage lautet nun: Ist x ein Substring von y, so dass gilt y(i,i+m-1) = x(1,m)? Weiterhin gilt folgendes Farbschema: - für y i = x j wird y i grün gekennzeichnet - für y i x j wird y i rot markiert - Ist x(1,m) ein Substring von y, wird x grün (fett) gekennzeichnet 3. Der Brute-Force Algorithmus Hierbei handelt es sich um den einfachsten und naheliegendsten Ansatz zur Stringsuche. Für jede Position im Text wird geprüft, ob das Pattern passt, d.h. eine Übereinstimmung beginnend an dieser Position gefunden wird. Der Start des Suchvorgangs erfolgt mit dem Vergleich y 1 =x 1, d.h. es werden die ersten beiden Zeichen verglichen. Anschließend werden solang die Indizes i und j um 1 inkrementiert, bis eine Nichtübereinstimmung x j y i gefunden wird. Daraufhin erfolgt eine Auswertung nach folgendem Schema: - Ist an der Stelle dieser Nichtübereinstimmung j > m, dann wurde das Pattern an der Position y i-m gefunden. Die Nichtübereinstimmung tritt nur auf, weil auf das Zeichen x m+1 zugegriffen wird (siehe Vereinbarungen). - Trift an dieser Stelle j m zu, dann wurde an der Position y i-j+1 kein Treffer gefunden. Mit folgenden Vorschriften wird die Suche fortgesetzt: - Index i wird auf ursprüngliche Startposition + 1 gesetzt. - Dem Index j wird der Wert 1 zugewiesen. Diese Suchvorschrift wird solange fortgesetzt, bis das Ende von y erreicht wird. Sollte bis zu diesem Zeitpunkt keine Übereinstimmung vorliegen, wird dem Index i, der "im Erfolgsfall" die Position des Patterns x in y angibt, der Wert 0 zugewiesen. Veranschaulichung: Text Pattern Vergleiche aaaaaaaaaa
4 An diesem Beispiel wird deutlich, dass unter Umständen die Suche nach dem Pattern sehr langwierig sein kann. Für den schlimmsten Fall, den sogenannten "worst case" beträgt die Laufzeit O(m n). Dieser Fall tritt für "normale" Texte recht selten auf, die durchschnittliche Laufzeit des Algorithmus ("average case") für englischen Text wurde in zahlreichen Experimenten mit O(n+m) ermittelt. Ungeachtet dieser Tatsache ist es sinnvoll, nach effizienteren Algorithmen zu suchen. 4. Der Algorithmus von Knuth-Morris-Pratt Der hier verwendete Ansatz entspricht dem des Brute-Force Algorithmus, allerdings wird hier ein Vorteil aus bereits verglichenen Zeichen gezogen. Der Ablauf der Suche entspricht ebenfalls dem des Brute-Force Algorithmus mit dem Unterschied, dass bei der hier besprochenen Version der Index i nicht zurückgesetzt wird. Bei einem Mismatch wird das Pattern "nach rechts verschoben", d.h. der Index i wird inkrementiert. Die Größe der Verschiebung des Patterns ist abhängig von der Struktur des Patterns sowie der Mismatchposition in y. Diese Informationen werden in einer sogenannten NextTable gespeichert. Dabei wird folgender Zusammenhang genutzt: Wenn ein Mismatch y i x j auftritt, ist bekannt, dass der Substring y(i-j+1,i-1) gleich dem Präfix x(1,j-1) ist. Veranschaulichung: Text xyzabcdabcdxyz Pattern abce Vergleich abce => Mismatch y 7 x 4 => x(1,3) = y(4,6) = 'abc' =>Der nächste Vergleich muss nicht bei y 5 starten, sondern kann bei y 8 beginnen. Wie wird die NextTable berechnet? Es wird untersucht, ob ein Präfix von x mit der Länge k-1 existiert, so dass dieses Präfix x(1,k-1) gleich dem Substring x(j-k+1,j-1) ist. Dazu wird für jede Position j in x das Präfix x(1,j-1) mit dem Präfix x(1,j-1) verglichen - also mit einer Kopie eben dieses Präfixes. Diese beiden Strings liegen zu Beginn um eins versetzt übereinander. Der erste Vergleich überprüft folglich, ob x 2 = x 1 gilt. Anschließend wird ein String so lang um 1 nach rechts verschoben, bis alle sich überlappenden Zeichen übereinstimmen oder keine Übereinstimmung gefunden wurde. Der Wert für die NextTable ergibt sich dann aus der Anzahl der Übereinstimmungen an der vorherigen Vergleichsposition plus 1, wobei next 1 := 0 gilt.
5 Beispiel für j=8 mit Pattern abcadabc => x(1,7) = abcadab abcadab => next 1 := 0 abcadab => next 2 := 1 abcadab => next 3 := 1 abcadab abcadab => next 4 := 1 abcadab => next 5 := 2 abcadab => next 6 := 1 abcadab => next 7 := 2 Da an der Vergleichsposition x 7 2 Zeichen übereinstimmen (x 1 = x 6 und x 2 = x 7 ), ergibt sich analog der Wert für next 8 := 3. NextTable: a b c a d a b c next 0 1 1 1 2 1 2 3 Die NextTable gibt also an, mit welchem Zeichen im Falle eines Mismatches an Position y i als nächstes verglichen werden soll. Hieraus ergibt sich eine Problematik, welche jedoch leicht zu beheben ist. Im Falle eines Mismatches y i x 8 im aktuellen Beispiel wird y i als nächstes mit x 3 verglichen, was wiederrum einem 'c' entspricht. Dieser Fall wird verhindert, indem in der Berechnungsvorschrift für die NextTable dieser Fall explizit überprüft und angepasst wird. Da dieses Verfahren lediglich eine Spezialversion des Brute-Force-Ansatzes ist, lässt sich mit dieser Vorgehensweise nur eine relativ kleine Verbesserung erzielen. Durch den Wegfall einiger unnötiger Vergleiche sinkt die "worst case"-abschätzung auf O(n+m) Vergleiche, wobei hier die Initialisierung der NextTable mit eingerechnet wurde. Der average case liegt bei O(n) Vergleichen. 5. Der Boyer-Moore Algorithmus Dieses Verfahren nutzt einen ähnlichen Ansatz wie der Algorithmus von Knuth-Morris-Pratt, allerdings wird hier der Vergleich im Pattern "von hinten" durchgeführt, d.h. solange x j = y i-m+j gilt, werden die Indizes i und j dekrementiert und ein erneuter Vergleich durchgeführt. Veranschaulichung: Text a string searching example Pattern sting => Da y 5 kein Substring von x ist, wird bereits an dieser Stelle klar, dass x(1,m) y(1,m). => a string searching example sting => Hier allerdings gilt: y 10 ist in x enthalten, deshalb kann eine Verschiebung um 5 Zeichen nicht durchgeführt werden (andernfalls könnte man Gefahr laufen, ein Auftreten des Patterns zu "verpassen") => a string searching example sting
6 Offensichtlich ist die Verschiebung des Patterns von dem Zeichen y i-m+j abhängig. Es wird hier also ebenfalls eine Tabelle benötigt, die die Informationen zur Verschiebung des Patterns speichert. Diese wird SkipTable genannt, da sie angibt, um wieviele Zeichen das Pattern verschoben wird. Dabei werden einige Zeichen übersprungen. Diese SkipTable wird durch Zeichen indiziert; dementsprechend hat sie die Größe des gültigen Alphabets, über dem x und y gebildet wurden. Die Berechnung der SkipTable ist denkbar einfach: for ω:=1 to Σ do skip ω :=m; for a:=1 to m do skip x[a] :=m-a; Beispiel: x = aedca => m = 5 [ω] a b c d e f g h... skip ω 0 5 1 2 3 5 5 5... Die Werte skip ω entsprechen dem Wert der Rechtsverschiebung des Patterns x im Text y, wenn ein Mismatch y i x j mit y i = ω aufgetreten ist. Dieser Algorithmus lässt sich noch weiter verbessern, indem zusätzlich zur SkipTable eine modifizierte NextTable eingesetzt wird (ähnlich wie im Knuth-Morris-Pratt Algorithmus). Aus diesen beiden Tabellen wird dann die optimale Verschiebung des Patterns berechnet und so eine weitere Einsparung an Vergleichen erzielt. Eine weitere Variante des Boyer-Moore Algorithmus - genannt Boyer-Moore Fast Skip - testet vor Beginn der Suche, wo im Text das Zeichen [ω] = x m auftritt, um weitere Mismatches bereits im voraus zu eliminieren. Um diese Information zu speichern, kann eine einfache Liste genutzt werden. Der Algorithmus von Boyer-Moore zeichnet sich durch erheblich kürzere Laufzeiten als die Algorithmen Brute-Force und Knuth-Morris-Pratt aus, da dieser auch nicht erfolgreiche Vergleiche zur Verbesserung seiner Laufzeit-Eigenschaften heranziehen kann. Durch die Nutzung zweier verschiedener Heuristiken beträgt die Abschätzung für den worst case zwar ebenfalls O(n+m), allerdings tritt dieser deutlich seltener auf. Der average case hingegen weist bei großen Σ und einem kurzen Pattern eine durchschnittliche Laufzeit von O(n/m) Vergleichen auf. Der zusätzliche Aufwand für die SkipTable beträgt m+ Σ und für die NextTable entsteht der gleiche Aufwand wie für den Algorithmus von Knuth-Morris-Pratt. 6. Der Algorithmus von Boyer-Moore-Horspool Der Algorithmus von Boyer-Moore spielt seine größten Vorteile bei stark selbstwiederholenden Suchmustern aus, welche allerdings vergleichsweise selten auftreten. Unter diesen Voraussetzungen wurde 1980 von Horspool eine vereinfachte Form dieses Algorithmus entwickelt, welche später nach ihm benannt wurde. Wie bereits bekannt, kann eine Match-Position nur dann vorliegen, wenn x m in y auftritt (an einer Position i). Wenn ein Mismatch bei x m = y i an der Position j in x auftritt, kann das Pattern weiter verschoben werden. Hier liegt der wesentliche Unterschied zum ursprünglichen Algorithmus von Boyer-Moore, welcher stets das Zeichen y i, das den Mismatch auslöste, verwendet, um die SkipTable zu indizieren. Das Pattern wird um den Wert, der dort gespeichert ist, verschoben.
7 Die Horspool-Variante hingegen berechnet die neue Vergleichsposition in y (an welcher dann x m ausgerichtet wird), indem zur Mismatchposition i der Wert von skip[x m ] addiert wird. Diese Art der Indizierung stellt zudem sicher, dass das Pattern für einen neuen Versuch niemals nach links verschoben wird. 7. Ausblick Suchalgorithmen bieten ein ausgesprochen weites Forschungsfeld. Nicht umsonst existieren die verschiedensten Ansätze zur Lösung des String-Matching-Problems. Angefangen von den hier erläuterten einfachen Verfahren über Algorithmen, die Hash-Werte verwenden, bis hin zu probabilistischen Verfahren existieren vielfältigste Algorithmen zum String-Matching- Problem. Nichtsdestotrotz kann es trotzdem sinnvoll sein, selbst nach Verbesserungen bzw. evtl. gar Neuentwicklungen von Algorithmen zu suchen. Eine Betrachtung verschiedener Algorithmen für den gleichen Einsatzzweck ist im Prozess der Software-Entwicklung von grundlegender Bedeutung. Hier lässt sich das größte Optimierungspotential hinsichtlich Laufzeitverhalten und Speicherplatzanforderungen ausmachen. Auch in jüngster Zeit werden ständig neue Algorithmen entwickelt. Dieses Dokument kann selbstverständlich nur eine verschwindend geringe Auswahl an Algorithmen berücksichtigen. Es wurde hier bewusst Wert auf grundlegende Erläuterungen gelegt, um nicht zu sehr ins Detail abzudriften. Für weiterführende Informationen zu diesen Algorithmen sei auf die Literatur [1] und [2] verwiesen. [1] R. Sedgewick, "Algorithmen", Addison-Wesley, 1992 [2] G. A. Stephen, "String Searching Algorithms", World Scientific Publ., 1994