String-Matching I (naiv, Rabin
Transcription
String-Matching I (naiv, Rabin
String-Matching I (naiv, Rabin-Karp) Robert Schimke ∗ 21. Oktober 2002 Problemstellung Einzelne Wörter oder größere Abschnitte in einem Text zu finden, ist ein häufiges Problem von Editoren, Textverarbeitungsprogrammen, aber auch von Suchmaschinen im Internet und, wenn man das Probelm abstrakter betrachtet, bei allen Aufgaben, bei denen ein bestimmtes Muster gefunden werden soll, z.B einzelne Sequenzen in einem DNA-Strang. Formal gestaltet sich das Problem folgendermaßen: Gegeben ist ein Text-Array T der Länge n und ein Muster -Array P (Pattern) der Länge m. Die Einträge in diesen Arrays sind Elemente eines endlichen Alphabetes Σ der Länge d, z.B die Zahlen von 0-1 oder der Ascii-Zeichensatz. Im folgenden wird solch ein Array auch als String bezeichnet. Ein Muster kommt an der Stelle s genau dann vor, wenn 0 6 s 6 n − m und T[s + 1..s + m] = P[1..m]. Die Aufgabe der folgenden Algorithmen ist es also, alle diese Stellen s zu finden. Text T Muster P a b c a b a a b c a b a c s=3 - a b a a Abbildung 1: Das String Matching Problem In diesem Beispiel kommt das Muster nur an der Stelle 3 vor Bezeichnungen: • Mit Σ∗ bezeichnet man alle möglichen endlichen Strings gebildet aus Elementen von Σ, einschließlich des leeren Strings ε • Die Länge eines Strings x wird mit |x| bezeichnet. • Die Verkettung von x und y, geschrieben als xy, hat die Länge |x| + |y| und besteht aus den Elementen von x gefolgt von denen aus y. • Ein String w ist Präfix von x (w@ x) genau dann, wenn x = wy . Ebenso ist w ein Suffix (wA x) genau dann, wenn x = yw, für ein y aus Σ∗ . • Die ersten k Zeichen P[1..k] eines Musters P[1..m] bezeichnen wir als Pk . ∗ e-mail: schimke.re@gmx.de 1 Aus den Bezeichnungen folgt folgendes Lemma (o.B.) Lemma 1 Es seien x,y,z Strings. Ferner sei x A y und y A z. Wenn |x| 6 |y| dann ist x A y. Ist |x| = |y|, dann ist x = y. 1 Naiver Algorithmus Der Naive-Algorithmus findet alle Stellen s, indem er für jede der n−m+1 möglichen Stellen überprüft, ob P[1..m] = T[s + 1..s + m] ist. 1 2 3 4 5 6 7 NAIVE_STRING_MATCHER (T, P) n:=|T| m:=|P| for(s:= 0 TO n-m) if (P[1..m]==T[s+1..s+m]) Print "Muster kommt an der Stelle" s "vor" Den naive Algorithmus kann man bildlich mit einer Schablone vergleichen, die Schritt für Schritt über den Text geschoben und bei jeder Stelle auf Übereinstimmung geprüft wird. a c a a b c a c a a b c x s=0 a a b a c a a b c a c a a b c x s=1 x s=2 a a b a a b Abbildung 2: Aufwand Der zeitlich Aufwand des naiven-Algorithmus liegt bei Θ((n−m+1)m) im WC. Wenn z.B der Text aus n a’s besteht und das gesuchte Muster aus m a’s, und man ferner davon ausgeht, dass der Vergleich aus Zeile 6 im WC (bei völliger Übereinstimmung) m Schritte benötigt, werden für jede der m-n+1 möglichen Stellen m Vergleichsschritte benötigt. Der große Nachteil des naiven-Algorithmus liegt in der Tatsache, dass an jeder Stelle s der Vergleich bei null anfängt und keinerlei Informationen aus den früheren Vergleichen nutzt. Ist das Muster z.B aaab und kommt an der Stelle 0 im Text vor, so kommen die Stellen 1-3 nicht in Frage, da T[4] b sein muss. 2 Rabin-Karp Agorithmus Der Algorithmus von Rabin und Karp nutzt elementare zahlentheoretische Eigenschaften. Der Einfachheit zu Liebe gehen wir im folgenden davon aus, dass die Einträge von Text und Muster dezimale Ziffern sind, also Σ = [0..9]. Einen String kann man somit als eine dezimale Zahl interpretieren. Der String 31415 entspricht der Zahl 31.415. Bei einem gegeben Muster P[1..m] bezeichne p die zugehörige Dezimalzahl, ferner bezeichne ts den zu dem Textabschnitt T[s+1..s+m] gehörenden Wert. P und 2 s=3 a a b T[s+1..s+m] sind also genau dann gleich, wenn p gleich ts ist. Wenn es möglich ist, p und ts in einer Zeit O(n) zu berechnen, dann kann man alle Stellen s, an denen das Muster auftritt, mit einem Aufwand O(n) bestimmen, indem man p mit allen ts vergleicht. p kann man sogar, durch Anwendung von dem Horner Schema mit einem Aufwand O(m) bestimmen: p = P[m] + 10(P[m − 1] + 10(P[m − 2] + . . . + 10([P [1]) · · ·)) t0 wird von T[1..m] ähnlich berechnet. Um die restlichen t1 . . . tn−m mit dem Aufwand O(n-m) zu berechnen genügt es zu zeigen, dass ts+1 von ts aus mit konstanten Aufwand berechnet werden kann: ts+1 = 10(ts − 10m−1 T[s + 1]) + T[s + m + 1] (1) Ist z.B. m = 5 und ts = 31415 dann wollen wir die vordere 3 entfernen und am Ende eine neue Ziffer einfügen. Sei T[s + 5 + 1] = 1, dann ist ts+1 = 10(31415 − 10000 · 3) + 2 = 14152 Wenn nun die Konstante 10m−1 vorberechnet wird (O(m)), hat jede Ausführung der Formel (1) einen konstanten Aufwand, somit können p und alle ts und somit der gesamte Algorithmus in einer Zeit von O(n+m) ausgeführt werden. Das Problem bei diesem Ansatz ist, dass p und somit die ts bei großem m so groß werden, dass Operationen auf ihnen nicht mehr mit konstantem Aufwand möglich sind. Der Ausweg, auf dem der Algorithmus von Rabin und Karp beruht, besteht darin, die Zahlen p und ts modulo eines geeigneten Modulos q zu berechnen. q ist eine Primzahl, so gewählt, dass d(Die Länge des Alphabets) · q immer noch in eine Speichereinheit passt, wobei d die Länge des Alphabetes ist. In unserem Fall ist also d = 10 und wir wählen q := 13. Gleichung (1) wird folgendermaßen angepasst: ts+1 = (d(ts − T[s + 1]h) + T[s + m + 1]) mod q (2) wobei d ≡ dm−1 ( mod q) ist. Als Beispiel sei T[s+1...s+m]=314152 und das nächste Element eine 2. Dann ist m=5, h=3 (10000 ≡ 3 mod 13), ts = 7 (31415 ≡ 7 mod 13) und ts+1 berechnet sich folgendermaßen 14152 ≡ ≡ ≡ ≡ (31415 − 3 · 10000) · 10 + 2 mod 13 (7 − 3 · 3) · 10 + 2 mod 13 (11) · 10 + 2 mod 13 8 mod 13 Der Nachteil ist, dass aus ts ≡ p mod q nicht unbedingt ts = p folgt, andererseits kann man aber sagen, dass aus ts 6≡ p sicher ts 6= p folgt. Somit kann man man alle möglichen Stellen s vorfiltern und muss nur noch einen Bruchteil der Zeichenfolgen auf Gleichheit untersuchen. Der folgende Pseudocode präzisiert diese Idee. Die Eingaben sind neben Text und Muster die Länge des Alphabets d und die zu benutzende Primzahl q. 3 2 3 5 9 0 2 3 1 4 1 5 2 6 7 3 9 9 2 1 @ @ @ mod 13 @ @ @ R @ @ R R @ Modulo-Wert 8 9 3 11 0 1 7 8 4 5 10 11 7 9 11 Text Treffer zusätzliche Übereinstimmung Abbildung 3: In diesem Beispiel sei das zu suchende Muster 314152 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 RABIN-KARP(T,P,d,q) n:=|T| m:=|P| h:=d^(m-1) mod q p:=0 t:=0 for (i:= 1 TO m) p := (dp + P[i] mod q t := (dt + T[i] mod q for (s:= 0 TO n-m) if p==t if (P[1..m]==T[s+1..s+m]) Print "Muster kommt an der Stelle "s" vor" if (s < n-m) t := (d(t-T[s+1]h)+T[s+m+1]) mod q Die Zeilen 2-4 ordnen den Variablen ihre Werte zu und berechnen den Wert von d. Die Zeilen 5-9 berechnen p und t0 jeweils mod q. Die For-Schleife in Zeile 10 geht nun alle möglichen Stellen durch und überprüft t und p auf Gleichheit. Nur in diesem Fall werden Muster und Textstelle verglichen. Am Ende der Schleife wird t nach Formel (2) der nächste Wert zugewiesen. Aufwand Im WC (T = am und P = am ) liegt der Aufwand genau wie beim ersten Algorithmus bei Θ((n − m + 1)m), da der Algorithmus jede möglich Stelle überprüfen muss und keine aussortiert werden können. Geht man aber nur von wenigen ”Treffern” aus, so kann man hoffen, dass die ts zufällig in den Bereich [1..q] gestreut werden. Dann benötigen die Zeilen 12-13 des Algorithmuses einen Aufwand von O(m(v+n/q)), wobei v die Anzahl der Treffer und n/q die Anzahl der sonstigen Übereinstimmungen von p und ts ist. Ist q > m und v relativ klein (O(1)) kann man den Aufwand mit O(n) abschätzen. Zusammen mit dem Aufwand O(n) der äußeren Schleife und O(m) der Berechnung von p und t0 liegt der Aufwand der Rabin-Karp-Algorithmus bei O(n+m) im AC. Literatur [Cor01] Thomas Cormen. Introduction to algorithms. zweite edition, 2001. [OW02] Thomas Ottmann and Peter Widmayer. Algorithmen und Datemstrukturen. Spektrum Lehrbuch, vierte edition, 2002. 4