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:
[email protected]
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