Kapitel 4 Algorithmen zur exakten Suche in Texten

Kapitel 4
Algorithmen zur exakten Suche
in Texten
In diesem Kapitel wird ein Problem aus der Sequenzanalyse näher
betrachtet, die exakte Textsuche: Gegeben ein Text und ein Muster,
finde alle Vorkommen von dem Muster in dem Text. Man wird sehen, dass zur Lösung dieses einfachen Problem einige nicht-triviale,
aber sehr effiziente Algorithmen entwickelt wurden. Zunächst soll jedoch ein kurzer Blick auf die Art und Weise geworfen werden, wie
Sequenzen üblicherweise in Java repräsentiert sind.
4.1
Die Klasse String
Zeichenketten sind in Java Objekte. Sie können mit dem +-Operator zu neuen
Objekten verknüpft werden.
String str1 = "hello";
String str2 = " world";
String str3 = str1 + str2;
Grundlegende objektbezogene Methoden der Klasse String:
public int length()
liefert die Anzahl der Zeichen in der Zeichenkette.
public int charAt(int index)
liefert das Zeichen an der Position index der Zeichenkette.
public int indexOf(int ch, int fromIndex)
liefert die erste Position ≥ fromIndex, an der das Zeichen ch in der Zeichenkette vorkommt, bzw. −1, falls das Zeichen nicht vorkommt.
43
public int lastIndexOf(int ch, int fromIndex)
liefert die letzte Position ≤ fromIndex, an der das Zeichen ch in der Zeichenkette vorkommt, bzw. −1, falls das Zeichen nicht vorkommt (lastIndexOf
liest also im Gegensatz zu indexOf die Zeichenkette von rechts nach links).
public boolean startsWith(String prefix)
liefert true gdw. die Zeichenkette mit dem angegebenen prefix beginnt.
public boolean endsWith(String suffix)
liefert true gdw. die Zeichenkette mit dem angegebenen suffix endet.
public String substring(int beginIndex, int endIndex)
liefert das Teilwort der Zeichenkette zwischen den Positionen beginIndex
und endIndex−1.
public char[] toCharArray()
wandelt die Zeichenkette in ein Feld von Zeichen um.
public boolean regionMatches (int start, String other, int ostart,
int len)
liefert true gdw. der Bereich der Zeichenkette mit dem Bereich der Zeichenkette String other übereinstimmt (s.u.). Der Vergleich beginnt bei der Position start bzw. ostart. Es werden nur die ersten len Zeichen verglichen.
public boolean equals(Object anObject)
liefert true gdw. die übergebene Objektreferenz auf ein Objekt vom Typ
String mit demselben Inhalt zeigt. (Dies steht im Gegensatz zu ==, was die
Objekt-(referenz-)gleichheit testet.)
Man beachte den Rückgabetyp int von charAt bzw. den Parametertyp int von
ch in indexOf und lastIndexOf. Dennoch können die Methoden auf (Unicode-)
Zeichen angewendet werden: Ein char-Wert in einem Ausdruck wird automatisch
in einen int-Wert umgewandelt, wobei die oberen 16 Bit auf Null gesetzt werden.
Beispiel 4.1.1
public class DemoStringClass {
public static void main(String[] args) {
String str = "bielefeld";
System.out.println(str.length());
System.out.println(str.charAt(0));
System.out.println(str.indexOf(’e’,3));
System.out.println(str.lastIndexOf(’e’,3));
System.out.println(str.startsWith("biele"));
System.out.println(str.endsWith("feld"));
44
System.out.println(str.substring(0,5));
System.out.println(str.substring(5,9));
System.out.println(str.regionMatches(0,"obiele",1,5));
if(str == args[0])
System.out.println("str == args[0]");
if(str.equals(args[0]))
System.out.println("str.equals(args[0])");
}
}
> java DemoStringClass bielefeld
9
b
4
2
true
true
biele
feld
true
str.equals(args[0])
4.2
Grundlegende Definitionen
Die Suche nach allen (exakten) Vorkommen eines Musters in einem Text ist ein
Problem, das häufig auftritt (z.B. in Editoren, Datenbanken etc.). Wir vereinbaren folgende Konventionen (wobei Σ ein Alphabet ist):
Muster P = P [0 . . . m − 1] ∈ Σm
Text T = T [0 . . . n − 1] ∈ Σn
P [j] = Zeichen an der Position j
P [b . . . e] = Teilwort von P zwischen den Positionen b und e
Beispiel 4.2.1 P = abcde, P [0] = a, P [3] = d, P [2 . . . 4] = cde.
Notation:
• leeres Wort: ε
• Länge eines Wortes x: |x|
• Konkatenation zweier Worte x und y: xy
45
Definition 4.2.2 Ein Wort w ist ein Präfix eines Wortes x (w ! x), falls x = wy
gilt für ein y ∈ Σ∗ . w ist ein Suffix von x (w " x), falls x = yw gilt für ein y ∈ Σ∗ .
x ist also auch ein Präfix bzw. Suffix von sich selbst.
Lemma 4.2.3 Es seien x, y und z Zeichenketten, so dass x " z und y " z gilt.
Wenn |x| ≤ |y| ist, dann gilt x " y. Wenn |x| ≥ |y| ist, dann gilt y " x. Wenn
|x| = |y| ist, dann gilt x = y.
Beweis: Übung (Aufgabe 4.7.1).
4.3
Das Problem der exakten Suche
Definition 4.3.1 Ein Muster P kommt mit der Verschiebung s im Text T vor,
falls 0 ≤ s ≤ n − m und T [s . . . s + m − 1] = P [0 . . . m − 1] gilt. In dem Fall
nennen wir s eine gültige Verschiebung.
Das Problem der exakten Textsuche ist nun, alle gültigen Verschiebungen zu
finden. Mit den Bezeichnungen S0 = ε und Sk = S[0 . . . k − 1] (d.h. Sk ist das
k-lange Präfix eines Wortes S ∈ Σ! für 0 ≤ k ≤ ") können wir das Problem der
exakten Textsuche folgendermaßen formulieren: Finde alle Verschiebungen s, so
dass P " Ts+m .
Beispiel 4.3.2
T = bielefeld oh bielefeld
P = feld
Gültige Verschiebungen: s = 5 und s = 18.
Die naive Lösung des Problems sieht in Pseudo-Code wie folgt aus:
Naive-StringMatcher(T, P )
1 n ← length[T ]
2 m ← length[P ]
3 for s ← 0 to n − m do
4
if P [0 . . . m − 1] = T [s . . . s + m − 1] then
5
print “Pattern occurs with shift” s
Am Beispiel T = abrakadabraxasa und P = abraxas erkennt man das ineffiziente Vorgehen, insbesondere wenn Text und Muster immer komplett verglichen
werden:
46
a b r a k a d a b r a x a s a
|
|
|
|
"
|
"
"
"
"
"
"
"
"
"
"
"
|
"
|
"
|
"
"
"
"
"
"
"
"
"
|
"
"
"
|
"
"
"
"
|
"
"
"
"
"
"
"
"
|
|
|
|
|
|
|
"
"
"
"
"
"
a b r a x a s
!
a b r a x a s
!
a b r a x a s
!
a b r a x a s
!
a b r a x a s
!
a b r a x a s
!
a b r a x a s
!
a b r a x a s
!
"
a b r a x a s
Eine Implementierung in Java verläuft analog:
public static void naiveMatcher(String text, String pattern) {
int m = pattern.length();
int n = text.length();
for(int s=0; s<=n-m; s++) {
if(text.regionMatches(s,pattern,0,m))
System.out.println("naiveMatcher: Pattern occurs with shift "+s);
}
}
Aber selbst wenn Text und Muster zeichenweise von links nach rechts und nur
1
bis zum ersten Mismatch
werden, ist die worst-case Zeiteffizienz des
! verglichen
"
naiven Algorithmus O n · m , z.B. wird für T = an , P = am die for-Schleife
n−m+1 mal ausgeführt und in jedem Test in Zeile 4 werden m Zeichen verglichen.
Wie kann man den naiven Algorithmus verbessern?
Idee 1: Überspringe ein Teilwort w von T , falls klar ist, dass w &= P (BMAlgorithmus 1977).
Idee 2: Merke Informationen über bisherige Vergleiche und nutze diese, um neue,
unnötige Vergleiche zu vermeiden (KMP-Algorithmus 1977).
1
Die zwei Zeichen stimmen nicht überein.
47
4.4
Der Boyer-Moore-Algorithmus
Der BM-Algorithmus legt wie der naive Algorithmus das Muster zunächst linksbündig an den Text, vergleicht die Zeichen des Musters dann aber von rechts
nach links mit den entsprechenden Zeichen des Textes. Beim ersten Mismatch
benutzt er zwei Heuristiken, um eine Verschiebung des Musters nach rechts zu
bestimmen.
4.4.1
Die bad-character Heuristik
Falls beim Vergleich von P [0 . . . m − 1] und T [s . . . s + m − 1] (von rechts nach
links) ein Mismatch
P [j] &= T [s + j] für ein j mit 0 ≤ j ≤ m − 1
festgestellt wird, so schlägt die bad-character Heuristik eine Verschiebung des
Musters um j − k Positionen vor, wobei k der größte Index (0 ≤ k ≤ m − 1) ist
mit T [s + j] = P [k]. Wenn kein k mit T [s + j] = P [k] existiert, so sei k = −1.
Man beachte, dass j − k negativ sein kann.
4.4.2
Die good-suffix Heuristik
Falls beim Vergleich von P [0 . . . m − 1] und T [s . . . s + m − 1] (von rechts nach
links) ein Mismatch
P [j] &= T [s + j] für ein j mit 0 ≤ j ≤ m − 1
festgestellt wird, so wird das Muster so weit nach rechts geschoben, bis das bekannte Suffix T [s + j + 1 . . . s + m − 1] wieder auf ein Teilwort des Musters passt.
Hierfür ist eine Vorverarbeitung des Musters notwendig, auf die wir hier aber
nicht näher eingehen wollen.
Im Boyer-Moore Algorithmus wird das Muster um das Maximum von beiden
vorgeschlagenen Verschiebungen verschoben. Es kann gezeigt werden, dass die
Laufzeitkomplexität dann im worst case O(n + m) ist.
Beispiel 4.4.1 Die Ausgangssituation ist in Abbildung 4.1(a) dargestellt. Die
bad-character Heuristik schlägt eine Verschiebung von j − k = (m − 3) − 5 =
(12 − 3) − 5 = 4 Positionen vor; dies ist im Fall (b) illustriert. Aus der Darstellung (c) wird ersichtlich, dass die good-suffix Heuristik eine Verschiebung von 3
Positionen vorschlägt. Also wird das Muster um max{4, 3} = 4 Positionen nach
rechts verschoben.
48
bad character
good suffix
"
"#$%&
... w r i t t e n _ n o t i c e _ t h a t ...
'
s! r e m i n i s c e n c e
(a)
... w r i t t e n _ n o t i c e _ t h a t ...
s+4
!
reminiscence
(b)
... w r i t t e n _ n o t i c e _ t h a t ...
s+3 ! r e m i n i s c e n c e
(c)
Abbildung 4.1: Verhalten des Boyer-Moore Algorithmus
4.5
Der Boyer-Moore-Horspool-Algorithmus
Der BM-Algorithmus verdankt seine Schnelligkeit vor allem der bad-charakter
Heuristik. Daher wurde 1980 von Horspool eine Vereinfachung des BM-Algorithmus
vorgeschlagen: Die bad-charakter Heuristik wird derart modifiziert, dass sie immer eine positive Verschiebung vorschlägt. Damit wird die good-suffix Heuristik
überflüssig (und auch die Vorverarbeitung einfacher).
Der BMH-Algorithmus geht in den meisten Fällen analog zur bisherigen badcharacter Heuristik vor.
Aber: Falls P [j] &= T [s + j] für ein j (0 ≤ j ≤ m − 1) gilt, so wird s um m − 1 − k
erhöht, wobei k der größte Index zwischen 0 und m−2 ist mit T [s+m−1] = P [k].
Wenn kein k (0 ≤ k ≤ m − 2) mit T [s + m − 1] = P [k] existiert, so wird s um m
erhöht. Das Muster wird also um
)
'
(
*
+,
λ T [s+m−1] = min {m}∪ m−1−k | 0 ≤ k ≤ m−2 und T [s+m−1] = P [k]
'
verschoben.
Wenn ein Match gefunden wurde, dann wird ebenfalls um λ T [s +
(
m − 1] verschoben.
Die Funktion λ lässt sich wie folgt berechnen:
computeLastOccurrenceFunction(P, Σ)
1 m ← length[P ]
2 for each character a ∈ Σ do
3
λ[a] = m
4 for j ← 0 to m − 2 do
49
... g o l d e n _ f l e e c e _ o f ...
'
s! r e m i n i s c e n c e
⇓
... g o l d e n _ f l e e c e _ o f ...
s+3 ! r e m i n i s c e n c e
Abbildung 4.2: Verhalten des BMH-Algorithmus bei einem Mismatch
... g o l d e n _ f l e e c e _ o f ...
s
!
fleece
⇓
... g o l d e n _ f l e e c e _ o f ...
s+2
!
fleece
Abbildung 4.3: Verhalten des BMH-Algorithmus bei einem Treffer
'
(
5
λ P [j] ← m − 1 − j
6 return λ
!
Die
worst-case-Komplexität
von
computeLastOccurrenceFunction
ist
O
|Σ| +
"
m .
Der BMH-Algorithmus sieht in Pseudo-Code folgendermaßen aus:
BMH-Matcher(T, P, Σ)
1 n ← length[T ]
2 m ← length[P ]
3 λ ← computeLastOccurrenceFunction(P, Σ)
4 s←0
5 while s ≤ n − m do
6
j ← m−1
7
while j ≥ 0 and P [j] = T [s + j] do
8
j ← j−1
9
if j = −1 then
10
print “Pattern
occurs (with shift” s
'
11
s ← s + λ T [s + m − 1]
Zur Verifikation, ob eine Verschiebung s gültig ist, wird O(m) Zeit benötigt.
50
!
Insgesamt
hat
der
BMH-Algorithmus
die
worst-case
Zeitkomplexität
O
n·m+
"
|Σ| (z.B. für T = an , P = am ).
4.6
4.6.1
Der Knuth-Morris-Pratt-Algorithmus
Einführendes Beispiel
Der naive Algorithmus ist ineffizient, da unabhängig von bereits stattgefundenen
erfolgreichen Vergleichen das Muster immer um ein Zeichen verschoben wird. Im
folgenden Beispiel ist dies noch einmal anhand der Suche nach dem Wort abraxas
im Text abrakadabra dargestellt.
a b r a
| | | |
a b r a
'
! a b r
'
! a b
|
! a
k a d a b r a
'
x a s
a x a s
r a x a s
'
b r a x a s
Man erkennt, dass im zweiten und dritten Schritt das Zeichen a an Position
1 im Muster mit den Zeichen b und r an der zweiten und dritten Position des
Textes verglichen wird, obwohl bereits nach dem positiven Vergleich von abra klar
sein musste, dass hier keine Übereinstimmung existieren kann. Verwendet man
dagegen Information aus vorangegangenen Vergleichen, so muss man nach einer
Verschiebung mit den zeichenweisen Vergleichen zwischen Muster und Text nicht
wieder am Anfang des Musters anfangen. Der Knuth-Morris-Pratt-Algorithmus
geht nach folgendem Schema vor: Wenn ein Teilwort (Präfix) des Musters bereits
erkannt wurde, aber dann ein mismatch auftritt, so ermittelt der Algorithmus
das längste Präfix dieses Teilwortes, das gleichzeitig echtes Suffix davon ist, und
schiebt das Muster dann so weit nach rechts, dass dieses Präfix an der bisherigen
Position des Suffixes liegt.
Beispiel 4.6.1 Im obigen Beispiel ist das bereits erkannte Teilwort abra. Das
längste Präfix von abra, das gleichzeitig echtes Suffix davon ist, ist a. Also schiebt
der Algorithmus das Muster so, dass das erste a von abraxas an der Position
steht, an der sich vorher das zweite a befunden hat. Der nächste stattfindende
Vergleich ist der von k mit b, da die Verschiebung ja gerade so durchgeführt
wurde, dass die beiden as übereinander liegen und somit nicht mehr verglichen
werden müssen.
51
a b r a k a d a b r a
| | | | '
a b r a x a s
'
! a b r a x a s
Man erkennt, dass hier drei unnötige Vergleiche vermieden wurden.
4.6.2
Funktionsweise
Beim KMP-Algorithmus wird das Muster P zeichenweise von links nach rechts
mit dem Text T verglichen. Es werden Informationen über bereits stattgefundene Vergleiche ausgenutzt, um weitere unnötige Vergleiche zu vermeiden. Als
Ausgangssituation sei Pq ein Präfix von P , das an Position s im Text vorkommt.
...
Pq
s!
Pq
...
Dann gehen wir gemäß der folgenden Fälle vor:
(A) q = m, d.h. s ist gültige Verschiebung:
1.
Bestimme k ∗ = π[m] = max{k : k < m und Pk " Pm }.
2.a) Falls k ∗ > 0, verschiebe das Muster nach rechts, so dass das Präfix Pk∗
von P unter dem Suffix Pk∗ von T [s . . . s + |P | − 1] liegt.
...
Pk ∗
...
Pk ∗
...
Pk ∗
...
⇓
Pk ∗
2.b) Falls k ∗ = 0, verschiebe das Muster P um die Länge |P |.
...
...
52
(B) q < m:
...
%
%
Pk
&#
Pq
Pk
&#
Pq
(a) a &= b und q &= 0:
$
$
a
...
b
• bestimme π[q] = max{k : k < q und Pk " Pq }, d.h. die Länge des
längsten Präfixes von P , das auch Suffix von Pq ist;
• verschiebe P nach rechts, so dass das Suffix Pk von Pq über dem
Präfix Pk von P liegt.
a
Pk
...
...
Pk
b
(b) a &= b und q = 0:
• verschiebe P um 1 nach rechts.
... a
...
b
(c) a = b:
• mache weiter mit q + 1.
...
Pq
a
Pq
a
...
Pq+1
s!
Pq+1
...
⇓
...
Beobachtung: Für jeden der vier Fälle gilt:
• das Muster wird nie nach links geschoben
• entweder wird ein neues Zeichen des Textes gelesen (B.c), oder das
Muster wird um mindestens eine Position nach rechts geschoben (A),
(B.a), (B.b).
53
Insgesamt können die Fälle also höchstens 2n-mal vorkommen.
Wir bestimmen die worst-case Zeitkomplexität des KMP-Algorithmus:
• Fälle (B.b) und (B.c) erfordern jeweils konstanten Zeitaufwand.
• In (A) und (B.a) wird die Funktion π benötigt mit π[q] = max{k : k < q
und Pk " Pq }, d.h. π[q] = Länge des längsten Präfixes von P , das ein
echtes Suffix von Pq ist. π hängt nur von P ab und kann daher in einem
Vorverarbeitungsschritt (V V ) berechnet und als Tabelle abgespeichert werden. Gegeben diese Tabelle, erfordern auch (A) und (B.a) nur konstanten
Zeitaufwand.
Damit ergeben sich folgende Komplexitäten: O(V V ) + O(n) Zeit und O(m) Speicherplatz im worst-case!
Beispiel 4.6.2 Die Präfixfunktion π : {1, 2, . . . , m} → {0, 1, . . . , m − 1} für ein
Muster P [0 . . . m − 1] ist definiert durch π[q] = max{k : k < q und Pk " Pq },
also die Länge des längsten Präfixes von P , das ein echtes Suffix von Pq ist. Sie
lautet für unser Beispielwort P = abrakadabra:
0 1 2 3 4 5 6 7 8 9 10 11
q
P [q] a b r a k a d a b r a ⊥
π[q] ⊥ 0 0 0 1 0 1 0 1 2 3 4
Für q = 9 wäre das entsprechende Teilmuster also abrakadab. Das längste Präfix,
das gleichzeitig echtes Suffix davon ist, ist ab. Es hat die Länge 2, darum findet
sich in der Tabelle an der Position q = 9 der Eintrag π[q] = 2.
Die Präfixfunktion π kann naiv folgendermaßen berechnet werden (die Laufzeit
beträgt O(m3 ), aber wir werden später sehen, wie es auch effizienter geht):
computePrefixFunction(P )
1 m ← length[P ]
2 π[1] ← 0
3 for q ← 2 to m do
4
k ← q−1
5
while k > 0 and P [0 . . . k − 1] &" P [0 . . . q − 1] do
6
k ← k−1
7
π[q] ← k
8 return π
Damit können wir den KMP-Algorithmus in Pseudo-Code formulieren:
KMP-Matcher(T, P )
1 n ← length[T ]
2 m ← length[P ]
54
3 π ← computePrefixFunction(P )
4 q←0
5 for i ← 0 to n − 1 do
6
while q > 0 and P [q] &= T [i] do
7
q ← π[q]
8
if P [q] = T [i] then
9
q ← q+1
10
if q = m then
11
print “Pattern occurs with shift” i − m + 1
12
q ← π[q]
Wir gehen nun der Frage nach, wie die Funktion π effizienter berechnet werden
kann. Unter der Annahme, dass die Werte von π für 1, . . . , q − 1 schon berechnet
sind, wollen wir π[q] berechnen. Gesucht:
π[q] = max{k : k < q und Pk " Pq }
Welche Pk kommen in Frage? Alle Pk = P [0 . . . k − 1] für die gilt:
Pk−1 " Pq−1
%&#$
%&#$
P [0...k−2]
und P [k − 1] = P [q − 1],
P [0...q−2]
also alle echten Suffixe von Pq−1 , die sich zu einem echten Suffix von Pq erweitern
lassen. Die Menge der Längen aller echten Suffixe von Pq−1 , die auch Präfixe von
P sind, ist:
{k − 1 : k − 1 < q − 1 : Pk−1 " Pq−1 } = {k $ : k $ < q − 1 : Pk" " Pq−1 }.
Damit muss gelten:
π[q] = max{k $ + 1 : k $ < q − 1, Pk" " Pq−1 und P [k $ ] = P [q − 1]}.
Nun kann man folgende Gleichheit zeigen (Lemma 4.6.4):
'
(
{k $ : k $ < q − 1 : Pk" " Pq−1 } = {π[q − 1], π π[q − 1] , π 3 [q − 1], . . . , π t [q − 1]},
% &# $
=π 2 [q−1]
wobei π t [q − 1] = 0 gilt. Schließlich erhalten wir:
π[q] = max{k $ + 1 : k ∈ {π[q − 1], . . . , π t [q − 1]} und P [k $ ] = P [q − 1]}.
Damit erhalten wir folgendes Verfahren, um π[q] zu berechnen: Wir schauen
π[q − 1] in der Tabelle von π nach und prüfen, ob sich das echte Suffix Pπ[q−1]
von Pq−1 zu einem echten Suffix von Pq erweitern lässt. Wenn ja, so ist π[q] =
55
π[q − 1] + 1. Wenn nein, so iterieren wir diesen Prozess, d.h. wir machen das Gleiche mit π(π[q − 1]) usw. Diese Überlegungen sollten die folgende Berechnungsmethode der Funktion π motivieren. Wir werden deren Korrektheit im nächsten
Unterabschnitt beweisen.
computePrefixFunction2(P )
1 m ← length[P ]
2 π[1] ← 0
3 k←0
4 for q ← 2 to m do
5
while k > 0 and P [k] &= P [q − 1] do
6
k ← π[k]
7
if P [k] = P [q − 1] then
8
k ← k+1
9
π[q] ← k
10 return π
Die Laufzeit von computePrefixFunction2 beträgt O(m). Dies kann man folgendermaßen sehen:
1. Die for-Schleife in Zeile 4 wird m − 1-mal durchlaufen, demzufolge auch
die Zeilen 7-9.
2. Der Wert von k ist anfangs 0 und wird insgesamt höchstens m − 1-mal in
Zeile 8 um jeweils 1 erhöht.
3. Bei jedem Durchlauf der while-Schleife in den Zeilen 5 und 6 wird der Wert
von k um mindestens 1 erniedrigt. Da k nicht negativ wird (Zeile 5), kann
die while-Schleife insgesamt also höchstens k-mal durchlaufen werden.
4.6.3
Korrektheit der Berechnung der Präfixfunktion
Das wesentliche Lemma im Korrektheitsbeweis besagt, dass man alle Präfixe
Pk , die Suffix eines gegebenen Präfixes Pq sind, berechnen kann, indem man die
Präfixfunktion π iteriert.
Definition 4.6.3 Es sei
*
+
π ∗ [q] = q, π[q], π 2 [q], π 3 [q], . . . , π t [q]
'
(
wobei π 0 [q] = q und π i+1 [q] = π π i [q] für i ≥ 0; dabei sei vereinbart, dass die
Folge in π ∗ [q] abbricht, wenn π t [q] = 0 erreicht wird.
Lemma 4.6.4 Es sei P [0 . . . m − 1] ein Muster und π die dazu gehörige Präfixfunktion.
Dann gilt π ∗ [q] = {k : Pk " Pq } für q = 1, . . . , m.
56
Beweis: Wir beweisen das Lemma, indem wir (1) π ∗ [q] ⊆ {k : Pk " Pq } und (2)
{k : Pk " Pq } ⊆ π ∗ [q] zeigen.
1. Wir zeigen: i ∈ π ∗ [q] impliziert Pi " Pq . Sei also i ∈ π ∗ [q]. Für i gibt es ein
u ∈ N mit i = π u [q]. Wir beweisen Pπu [q] " Pq durch Induktion über u.
Induktionsanfang: u = 0. Dann ist i = π 0 [q] = q und die Behauptung gilt.
Induktionshypothese: Für i0 = π u0 [q] gilt Pi0 " Pq .
Induktionsschritt:
Zu zeigen ist: für i = π u0 +1 [q] gilt Pi " Pq . Es gilt i =
' u (
π π 0 [q] = π[i0 ]. Mit der Induktionshypothese folgt Pi0 " Pq . Weiterhin
gilt
i = π[i0 ] = max{k : k < i0 und Pk " Pi0 },
also gilt Pi " Pi0 " Pq . Da " transitiv ist, folgt Pi " Pq .
2. Wir beweisen {k : Pk " Pq } ⊆ π ∗ [q] durch Widerspruch. Wir nehmen an
∃j ∈ {k : Pk " Pq }\π ∗ [q].
Ohne Einschränkung der Allgemeinheit sei j die größte Zahl mit dieser
Eigenschaft. Da
j ≤ q und q ∈ {k : Pk " Pq } ∩ π ∗ [q]
gilt, folgt j < q. Es sei j $ die kleinste Zahl in π ∗ [q], die größer als j ist (da
q ∈ π ∗ [q] gilt, existiert solch ein j $ immer). Wegen j ∈ {k : Pk " Pq } gilt
Pj " Pq . Weiterhin impliziert j $ ∈ π ∗ [q] wg. (1) Pj " " Pq . Mit j < j $ folgt
damit Pj " Pj " . Es gibt kein
j $$ , j < j $$ < j $ , mit Pj " Pj "" " Pj "
Gäbe es solch ein j $$ , so müsste j $$ ∈ π ∗ [q] gelten, denn j ist die größte Zahl
mit
j ∈ {k : Pk " Pq }\π ∗ [q].
j $$ ∈ π ∗ [q] kann aber nicht gelten, denn j $ ist die kleinste Zahl in π ∗ [q], die
größer als j ist. Also muss j = max{k : k < j $ und Pk " Pj " } = π[j $ ] und
damit j ∈ π ∗ [q] gelten. Dieser Widerspruch beweist {k : Pk " Pq } ⊆ π ∗ [q].
#
Definition 4.6.5 Für q = 2, 3, . . . , m sei
*
+
Eq−1 = k : k < q − 1, k ∈ π ∗ [q − 1], P [k] = P [q − 1] .
Die Menge Eq−1 ⊆ π ∗ [q − 1] enthält alle
k ∈ π ∗ [q − 1] = {k : Pk " Pq−1 }
mit k < q − 1 und Pk+1 " Pq ; denn aus
Pk = P [0 . . . k − 1] " Pq−1 = P [0 . . . q − 2] und P [k] = P [q − 1]
folgt Pk+1 = P [0 . . . k] " Pq = P [0 . . . q − 1]).
57
Lemma 4.6.6 Sei P [0 . . . m−1] ein Muster und π die zugehörige Präfixfunktion.
Für q = 2, 3, . . . , m gilt:
0
falls Eq−1 = ∅
π[q] =
1 + max{k ∈ Eq−1 } falls Eq−1 &= ∅
Beweis: Mit der Vereinbarung max{} = 0 gilt:
*
+
π[q]
=
max *k : k < q und Pk " Pq
+
=
max k : k < q, Pk−1 " Pq−1 und P [k − 1] = P [q − 1]
*
+
(4.6.4)
=
max k : k − 1 < q − 1, k − 1 ∈ π ∗ [q − 1] und P [k − 1] = P [q − 1]
*
+
k" =k−1
$
∗
$
=
max *k $ + 1 : k $ < q − 1,
k
∈
π
[q
−
1]
und
P
[k
]
=
P
[q
−
1]
+
=
max k $ + 1 : k $ ∈ Eq−1
Fall 1: Eq−1 = ∅: Dann gilt π[q] = max{} = 0.
Fall 2: Eq−1 &= ∅: Dann gilt π[q] = max{1 + k $ : k $ ∈ Eq−1 } = 1 + max{k $ : k $ ∈
Eq−1 }.
#
Satz 4.6.7 computePrefixFunction2(P) berechnet die Funktion π korrekt.
Beweis: Am Anfang jeder Iteration der for-Schleife gilt k = π[q − 1], denn beim
ersten Betreten der for-Schleife gilt k = 0, q = 2 und π[q − 1] = 0 = k und diese
Eigenschaft bleibt wegen Zeile 9 bei jeder Iteration der for-Schleife erhalten. Die
Eigenschaft k = π[q − 1] wird daher auch Invariante der Schleife genannt. Die
while-Schleife untersucht alle Werte
k ∈ π ∗ [q − 1]\{q − 1},
bis einer mit P [k] = P [q − 1] gefunden wird; an diesem Punkt gilt
k = max{k ∈ Eq−1 },
so dass π[q] den Wert k + 1 = 1 + max{k ∈ Eq−1 } erhält. Wird kein solcher Wert
k gefunden, so wird π[q] der Wert 0 zugewiesen, denn es gilt k = 0 in den Zeilen
7-9.
Das in Zeile 10 zurückgegebene Feld π hat also folgende Werte:
π[1] = 0 und für q = 2, 3, . . . , m
.
0
π[q] =
1 + max{k ∈ Eq−1 }
falls Eq−1 = ∅
falls Eq−1 &= ∅
#
58
4.7
Aufgaben
Aufgabe 4.7.1 Beweisen Sie Lemma 4.2.3.
Aufgabe 4.7.2 Implementieren Sie den Boyer-Moore-Horspool-Algorithmus in
Java.
Aufgabe 4.7.3 Bestimmen Sie die worst-case Zeiteffizienz der naiven Implementierung von computePrefixFunction.
Aufgabe 4.7.4 Implementieren Sie den Knuth-Morris-Pratt-Algorithmus in Java.
Aufgabe 4.7.5 Geben Sie konkrete Beispiele an, in denen
(a) der Knuth-Morris-Pratt-Algorithmus
(b) der Boyer-Moore-Horspool-Algorithmus
(c) der Boyer-Moore-Algorithmus
sich besser verhält als die beiden anderen (unter Vernachlässigung der Vorverarbeitung). Das Kriterium ist hierbei die Anzahl der Vergleiche von Zeichen, die
jeweils durchgeführt werden müssen.
Aufgabe 4.7.6 Man erreicht eine verbesserte Version des Knuth-Morris-PrattMatchers, indem man π in der Zeile 7 (nicht aber in Zeile 12) durch π $ ersetzt.
π $ ist rekursiv für q = 1, 2, . . . , m − 1 definiert:

wenn π[q] = 0,
 0
π $ [π[q]]
wenn π[q] &= 0 und P [π[q]] = P [q],
π $ [q] =

π[q]
wenn π[q] &= 0 und P [π[q]] &= P [q].
Erklären Sie, warum der modifizierte Algorithmus korrekt ist und inwiefern diese Modifikation eine Verbesserung darstellt. Stellen Sie die Funktion π $ für das
Muster P = ababababca tabellarisch dar.
Aufgabe 4.7.7 Geben Sie einen Algorithmus an, der herausfindet, ob ein Text
T eine zyklische Verschiebung eines Textes T $ ist. Zum Beispiel ist der Text reif
eine zyklische Verschiebung von frei. Der Algorithmus soll linear im Zeitaufwand
sein. Implementieren Sie Ihren Algorithmus in Java.
Aufgabe 4.7.8 Erklären Sie, wie man alle Vorkommen des Musters P im Text
T unter Zuhilfenahme der π Funktion für die Zeichenkette P T bestimmen kann.
P T ist die Konkatenation von P und T (also m+n Zeichen lang). Implementieren
Sie Ihren Vorschlag in Java.
59
60