03.07.2013 Views

Algoritmen en Datastructuren III Partim: Algoritmen voor strings - caagt

Algoritmen en Datastructuren III Partim: Algoritmen voor strings - caagt

Algoritmen en Datastructuren III Partim: Algoritmen voor strings - caagt

SHOW MORE
SHOW LESS

You also want an ePaper? Increase the reach of your titles

YUMPU automatically turns print PDFs into web optimized ePapers that Google loves.

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong><br />

<strong>Partim</strong>: <strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> <strong>strings</strong><br />

Veerle Fack<br />

3 december 2008


Inhoudsopgave<br />

1 <strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> exact string-matching 1<br />

1.1 E<strong>en</strong> e<strong>en</strong>voudig brute-kracht-algoritme . . . . . . . . . . . . . . . . . . . . . . . 2<br />

1.2 Het algoritme van Rabin-Karp . . . . . . . . . . . . . . . . . . . . . . . . . . . 3<br />

1.3 Het algoritme van Knuth-Morris-Pratt . . . . . . . . . . . . . . . . . . . . . . . 6<br />

1.3.1 Inefficiëntie van het brute-kracht-algoritme . . . . . . . . . . . . . . . . 6<br />

1.3.2 Gebruik van e<strong>en</strong> verschuivingstabel <strong>voor</strong> het patroon . . . . . . . . . . . 7<br />

1.3.3 Het algoritme van Knuth-Morris-Pratt . . . . . . . . . . . . . . . . . . . 9<br />

1.3.4 Berek<strong>en</strong><strong>en</strong> van de verschuivingstabel . . . . . . . . . . . . . . . . . . . 11<br />

1.4 Het algoritme van Boyer-Moore-Horspool . . . . . . . . . . . . . . . . . . . . . 12<br />

1.4.1 De occurr<strong>en</strong>ce-heuristiek . . . . . . . . . . . . . . . . . . . . . . . . . . 12<br />

1.4.2 De match-heuristiek . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14<br />

1.4.3 Het algoritme van Boyer-Moore-Horspool . . . . . . . . . . . . . . . . . 15<br />

2 <strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> b<strong>en</strong>ader<strong>en</strong>d string-matching 19<br />

2.1 Editeerafstand tuss<strong>en</strong> <strong>strings</strong> . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20<br />

2.2 Beste b<strong>en</strong>ader<strong>en</strong>de match . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23<br />

i


Hoofdstuk 1<br />

<strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> exact<br />

string-matching<br />

Het zoek<strong>en</strong> van patron<strong>en</strong> in tekst is e<strong>en</strong> alomteg<strong>en</strong>woordig probleem, overal waar informatie<br />

bijgehoud<strong>en</strong> <strong>en</strong> opgeslag<strong>en</strong> wordt. Toepassing<strong>en</strong> gaan van zoekrobots op het Internet tot het<br />

zoek<strong>en</strong> van woord<strong>en</strong> in bestand<strong>en</strong> op de eig<strong>en</strong> computer, ev<strong>en</strong>als het zoek<strong>en</strong> van sequ<strong>en</strong>ties<br />

in biologische databank<strong>en</strong>. De algem<strong>en</strong>e vorm van het probleem bestaat erin om in e<strong>en</strong> tekst<br />

e<strong>en</strong> match <strong>voor</strong> e<strong>en</strong> patroon te vind<strong>en</strong>, waarbij de begripp<strong>en</strong> “tekst”, “patroon” <strong>en</strong> “match” op<br />

verschill<strong>en</strong>de manier<strong>en</strong> kunn<strong>en</strong> geïnterpreteerd word<strong>en</strong>.<br />

1


2 Hoofdstuk 1. <strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> exact string-matching<br />

In dit deel zull<strong>en</strong> we de volg<strong>en</strong>de terminologie gebruik<strong>en</strong>. Meestal noter<strong>en</strong> we door P het patroon<br />

dat gezocht wordt, <strong>en</strong> door T de tekst waarin het patroon gezocht wordt. Doorgaans veronderstell<strong>en</strong><br />

we ook dat het onderligg<strong>en</strong>de alfabet Σ vast is. Meestal bestaat dit alfabet uit de gebruikelijke<br />

letters, cijfers <strong>en</strong> leestek<strong>en</strong>s. In bepaalde gevall<strong>en</strong> zull<strong>en</strong> we ons echter beperk<strong>en</strong> tot het binaire<br />

alfabet Σ = {0,1}. De verzameling van alle (eindige) woord<strong>en</strong> op het alfabet Σ wordt g<strong>en</strong>oteerd<br />

als Σ ∗ ; {0,1} ∗ is dus de verzameling van alle binaire <strong>strings</strong>.<br />

De l<strong>en</strong>gte van e<strong>en</strong> string S noter<strong>en</strong> we door |S|. Meestal noter<strong>en</strong> we m = |P| <strong>en</strong> n = |T|, <strong>en</strong> we<br />

veronderstell<strong>en</strong> dat m ≤ n. Door P[i.. j] noter<strong>en</strong> we de substring van P die begint op positie i <strong>en</strong><br />

eindigt op positie j. Dus P[0..m−1] is de ganse string P, <strong>en</strong> P[0..i] is e<strong>en</strong> prefix van P. Als i > j,<br />

dan definiër<strong>en</strong> we P[i.. j] als de lege string ε (dit is de unieke string van l<strong>en</strong>gte 0). We schrijv<strong>en</strong><br />

ook P[i] <strong>voor</strong> P[i..i], zodat P[0] de eerste <strong>en</strong> P[m − 1] de laatste letter van P is.<br />

In het e<strong>en</strong>voudigste geval is de tekst e<strong>en</strong> docum<strong>en</strong>t in het computergeheug<strong>en</strong> (bij<strong>voor</strong>beeld in<br />

e<strong>en</strong> tekstverwerker) <strong>en</strong> het patroon is e<strong>en</strong> woord dat m<strong>en</strong> in dit docum<strong>en</strong>t wil localiser<strong>en</strong>. Dit<br />

is de klassieke versie van het “text searching” probleem, dat door e<strong>en</strong> brute-kracht-algoritme in<br />

O(|P| × |T|) sequ<strong>en</strong>tiële tijd kan opgelost word<strong>en</strong>. We besprek<strong>en</strong> dit algoritme in Paragraaf 1.1.<br />

In Paragraaf 1.2 besprek<strong>en</strong> we de oplossing van Rabin-Karp, die gemiddelde uitvoeringstijd<br />

O(|P|+|T|) heeft. Deze oplossing gebruikt de techniek van hashing, hier “fingerprinting” g<strong>en</strong>oemd.<br />

Het klassieke probleem kan opgelost word<strong>en</strong> in O(|P| + |T|) uitvoeringstijd. De twee meest<br />

gek<strong>en</strong>de algoritm<strong>en</strong> die dit verwez<strong>en</strong>lijk<strong>en</strong> zijn het algoritme van Knuth-Morris-Pratt (zie Paragraaf<br />

1.3) <strong>en</strong> het algoritme van Boyer-Moore (zie Paragraaf 1.4). Het implem<strong>en</strong>ter<strong>en</strong> van het<br />

algoritme van Boyer-Moore is echter ingewikkeld, zodat hier<strong>voor</strong> e<strong>en</strong> variant ontwikkeld is, het<br />

zog<strong>en</strong>aamde algoritme van Boyer-Moore-Horspool (zie ook Paragraaf 1.4). Dit algoritme heeft<br />

e<strong>en</strong> slechtste-geval-uitvoeringstijd O(|P| × |T |), maar werkt in de praktijk zeer snel <strong>en</strong> is ook<br />

e<strong>en</strong>voudig te implem<strong>en</strong>ter<strong>en</strong>.<br />

1.1 E<strong>en</strong> e<strong>en</strong>voudig brute-kracht-algoritme<br />

Beschouw e<strong>en</strong> tekstverwerker die e<strong>en</strong> woord (of e<strong>en</strong> frase) in e<strong>en</strong> docum<strong>en</strong>t moet zoek<strong>en</strong>. E<strong>en</strong><br />

e<strong>en</strong>voudige b<strong>en</strong>adering localiseert het woord door het letter per letter te vergelijk<strong>en</strong> met de tekst,<br />

daarbij alle mogelijke posities in de tekst prober<strong>en</strong>d. Algoritme 1.1 implem<strong>en</strong>teert dit idee.<br />

Stelling 1.1.1. Het brute-kracht-algoritme 1.1 vindt de correcte match van e<strong>en</strong> patroon in e<strong>en</strong><br />

tekst (wanneer die bestaat) in uitvoeringstijd O(m(n − m+1)).<br />

Om te argum<strong>en</strong>ter<strong>en</strong> dat het algoritme correct werkt, moet<strong>en</strong> we nagaan dat het patroon gecontroleerd<br />

wordt op alle mogelijke posities in de tekst. Aangezi<strong>en</strong> het patroon m karakters bevat, is<br />

de laatst mogelijke positie <strong>voor</strong> P in T = [0..n − 1] gegev<strong>en</strong> door T[n − m..n − 1]. De buit<strong>en</strong>ste<br />

while-lus probeert alle mogelijke indices van 0 t.e.m. n − m.<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


1.2. Het algoritme van Rabin-Karp 3<br />

Algoritme 1.1 (Brute-kracht-algoritme) Zoek<strong>en</strong> naar e<strong>en</strong> patroon P in e<strong>en</strong> tekst T<br />

Input: patroon P van l<strong>en</strong>gte m, tekst T van l<strong>en</strong>gte n<br />

Output: de kleinste index i zodanig dat T[i..i+m−1] = P, of −1 wanneer P niet optreedt in T<br />

1: Stel i ← 0<br />

2: while i+m ≤ n do<br />

3: Stel j ← 0<br />

4: while T[i+ j] = P[ j] do<br />

5: Stel j ← j+ 1<br />

6: if j ≥ m th<strong>en</strong><br />

7: return i<br />

8: Stel i ← i+1<br />

9: return −1<br />

In het slechtste geval is er ge<strong>en</strong> overe<strong>en</strong>stemming <strong>voor</strong> het patroon in de tekst, <strong>en</strong> dan heeft het<br />

algoritme e<strong>en</strong> Θ(m(n−m+1)) uitvoeringstijd. In het beste geval wordt het patroon aan het begin<br />

van de tekst gevond<strong>en</strong>, <strong>en</strong> dan is de uitvoeringstijd Θ(m).<br />

In de praktijk gedraagt het algoritme zich beter dan O(m(n−m+1)), omdat de binn<strong>en</strong>ste whilelus<br />

e<strong>en</strong> niet-overe<strong>en</strong>stemm<strong>en</strong>d patroon behoorlijk snel herk<strong>en</strong>t. M<strong>en</strong> kan bewijz<strong>en</strong> dat, <strong>voor</strong><br />

willekeurige patron<strong>en</strong> <strong>en</strong> tekst<strong>en</strong>, de uitvoeringstijd O(n − m) is (zonder bewijs).<br />

Dit e<strong>en</strong>voudige algoritme kan zeer inefficiënt word<strong>en</strong> <strong>voor</strong> langere patron<strong>en</strong>, in het bijzonder<br />

wanneer in het patroon <strong>en</strong> de tekst stukk<strong>en</strong> <strong>voor</strong>kom<strong>en</strong> die zich herhal<strong>en</strong>. De uitvoeringstijd<br />

kan O(n 2 ) word<strong>en</strong>, bv. wanneer m = n/2. Onze uiteindelijke bedoeling is het ontwikkel<strong>en</strong> van<br />

e<strong>en</strong> algoritme dat in het slechtste geval lineaire uitvoeringstijd O(n + m) heeft. E<strong>en</strong> dergelijk<br />

algoritme gev<strong>en</strong> we in Paragraaf 1.3. In de volg<strong>en</strong>de paragraaf besprek<strong>en</strong> we e<strong>en</strong> algoritme dat<br />

e<strong>en</strong> gemiddelde O(n+m) uitvoeringstijd heeft.<br />

1.2 Het algoritme van Rabin-Karp<br />

In dit geval beperk<strong>en</strong> we ons tot het binaire alfabet Σ = {0,1}. Het c<strong>en</strong>trale idee achter het algoritme<br />

van Rabin-Karp is het volg<strong>en</strong>de: <strong>voor</strong>aleer we tijd sp<strong>en</strong>der<strong>en</strong> om effectief te controler<strong>en</strong> of<br />

e<strong>en</strong> lang patroon op e<strong>en</strong> bepaalde positie optreedt (dit kan m stapp<strong>en</strong> vereis<strong>en</strong>), prober<strong>en</strong> we eerst<br />

door <strong>en</strong>kele e<strong>en</strong>voudige test<strong>en</strong> na te gaan of we het gegev<strong>en</strong> patroon op de gegev<strong>en</strong> positie kunn<strong>en</strong><br />

verwacht<strong>en</strong>. Veronderstel dat we e<strong>en</strong> dergelijke test in e<strong>en</strong> goedkope O(1) tijd kunn<strong>en</strong> uitvoer<strong>en</strong>,<br />

<strong>en</strong> dat we kunn<strong>en</strong> verwacht<strong>en</strong> daarmee alle behalve 1/m-de van de posities te eliminer<strong>en</strong>, m.a.w.<br />

we houd<strong>en</strong> (n−m+1)/m posities over waar<strong>voor</strong> we effectief de O(m) brute-kracht-vergelijking<br />

moet<strong>en</strong> doorvoer<strong>en</strong>. Dit levert gezamelijke uitvoeringstijd O(m(n − m+1)/m) <strong>voor</strong> de vergelijking<strong>en</strong>.<br />

Sam<strong>en</strong> met de O(m) tijd <strong>voor</strong> de <strong>voor</strong>afgaande test<strong>en</strong>, geeft dit aanleiding tot e<strong>en</strong> totale<br />

gemiddelde uitvoeringstijd van O(n+m).<br />

Hoe kunn<strong>en</strong> we nu snel bepaalde posities eliminer<strong>en</strong>? Veronderstel bij<strong>voor</strong>beeld dat we zoek<strong>en</strong><br />

naar het patroon P = “000011” in de tekst T = “0000010000100000”. Het patroon (van l<strong>en</strong>gte 6)<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


4 Hoofdstuk 1. <strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> exact string-matching<br />

heeft pariteit 2 (e<strong>en</strong> ev<strong>en</strong> aantal 1-<strong>en</strong>). In de tekst daar<strong>en</strong>teg<strong>en</strong> heeft elke deelstring van l<strong>en</strong>gte 6<br />

pariteit 1, behalve “100001”. Van de 11 = 16 − 6 + 1 posities in de tekst kunn<strong>en</strong> we er 10<br />

eliminer<strong>en</strong>, simpelweg omdat ze de verkeerde pariteit hebb<strong>en</strong>. Rabin <strong>en</strong> Karp noemd<strong>en</strong> deze<br />

methode fingerprinting, omdat we niet het volledige patroon vergelijk<strong>en</strong> maar slechts één klein<br />

aspect ervan (zijn vingerafdruk).<br />

We kunn<strong>en</strong> dus het e<strong>en</strong>voudige algoritme uitbreid<strong>en</strong> om eerst e<strong>en</strong> pariteitscontrole te do<strong>en</strong> <strong>en</strong><br />

posities met de verkeerde pariteit over te slaan. E<strong>en</strong> rechtlijnige implem<strong>en</strong>tatie van dit idee zal<br />

echter niet tot e<strong>en</strong> verbeterde uitvoeringstijd leid<strong>en</strong>, omdat het berek<strong>en</strong><strong>en</strong> van de pariteit van<br />

m bits ook m stapp<strong>en</strong> vraagt, terwijl we eis<strong>en</strong> dat de test in constante tijd gebeurt. We kunn<strong>en</strong> dit<br />

echter als volgt bekom<strong>en</strong>. Bij het begin van het algoritme bepal<strong>en</strong> we de pariteit van P <strong>en</strong> van<br />

de eerste m bits van de tekst, nl. T[0..m − 1]; dit vraagt O(m) tijd. Voor elke stap i > 0 hebb<strong>en</strong><br />

we de pariteit van T[i..i+m−1] berek<strong>en</strong>d, zodat we in de volg<strong>en</strong>de stap i+1 de pariteit van<br />

T[i+1..i+m] kunn<strong>en</strong> berek<strong>en</strong><strong>en</strong> door <strong>en</strong>kel de bits T[i] <strong>en</strong> T[i+m] bekijk<strong>en</strong>. Immers, zij x de<br />

pariteit van T[i..i+m−1], dan bekom<strong>en</strong> we de pariteit van T[i+1..i+m] als<br />

x+(T[i]+T[i+m])mod 2.<br />

De totale tijd besteed aan de <strong>voor</strong>afgaande tests <strong>voor</strong> e<strong>en</strong> tekst van l<strong>en</strong>gte n is dus O(n), of<br />

uitgemiddeld O(1) per test.<br />

Gemiddeld kunn<strong>en</strong> we verwacht<strong>en</strong> dat de pariteitscontrole de helft van de posities elimineert,<br />

zodat de lus ongeveer m(n − m + 1)/2 stapp<strong>en</strong> heeft. Dit is echter nog steeds Θ(m(n − m +<br />

1)). Pariteitscontrole alle<strong>en</strong> is dus niet voldo<strong>en</strong>de om het algoritme op de gew<strong>en</strong>ste manier te<br />

versnell<strong>en</strong>. De red<strong>en</strong> hier<strong>voor</strong> is dat teveel waard<strong>en</strong> dezelfde vingerafdruk del<strong>en</strong>. Wanneer we<br />

e<strong>en</strong> versnellingsfactor van q will<strong>en</strong> bekom<strong>en</strong>, dan moet<strong>en</strong> we e<strong>en</strong> vingerafdrukfunctie vind<strong>en</strong> die<br />

(a) bit<strong>strings</strong> van l<strong>en</strong>gte m op q verschill<strong>en</strong>de waard<strong>en</strong> (vingerafdrukk<strong>en</strong>) afbeeldt;<br />

(b) de bit<strong>strings</strong> van l<strong>en</strong>gte m gelijkmatig verdeelt over de q vingerafdrukk<strong>en</strong>;<br />

(c) gemakkelijk “in sequ<strong>en</strong>tie” te berek<strong>en</strong><strong>en</strong> is. Hiermee bedoel<strong>en</strong> we dat, bij wijzig<strong>en</strong> van de<br />

string met 1 bit <strong>en</strong>kel aan het begin <strong>en</strong> het einde, de nieuwe vingerafdruk in O(1) tijd te<br />

berek<strong>en</strong><strong>en</strong> is.<br />

Pariteitscontrole voldoet aan alle <strong>voor</strong>waard<strong>en</strong> <strong>voor</strong> q = 2, hetge<strong>en</strong> verklaart waarom dit e<strong>en</strong><br />

versnellingsfactor 2 geeft.<br />

E<strong>en</strong> functie die aan <strong>voor</strong>waard<strong>en</strong> (a) <strong>en</strong> (b) voldoet, is e<strong>en</strong> hashfunctie. Deze <strong>voor</strong>waard<strong>en</strong> zijn<br />

nodig omdat ze implicer<strong>en</strong> dat gemiddeld (q − 1)/q van de waard<strong>en</strong> geëlimineerd word<strong>en</strong>, zodat<br />

het brute-kracht-algoritme slechts op 1/q van de posities moet uitgevoerd word<strong>en</strong>. Dit betek<strong>en</strong>t,<br />

<strong>voor</strong> (n − m+1)/q posities, e<strong>en</strong> vergelijking die O(m) tijd kost, <strong>en</strong> dus in totaal e<strong>en</strong> tijd m(n −<br />

m+1)/q. Wanneer we q groter dan m kiez<strong>en</strong>, is dit O(n).<br />

Voorwaarde (c) garandeert dat we in constante tijd kunn<strong>en</strong> besliss<strong>en</strong> of e<strong>en</strong> positie geëlimineerd<br />

kan word<strong>en</strong>. Het eliminer<strong>en</strong> van de posities kost dus in totaal O((n − m + 1)(q − 1)/q) tijd,<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


1.2. Het algoritme van Rabin-Karp 5<br />

hetge<strong>en</strong> O(n) is. Br<strong>en</strong>g<strong>en</strong> we ook de O(m) <strong>voor</strong>bereidingstijd in rek<strong>en</strong>ing, dan kom<strong>en</strong> we e<strong>en</strong><br />

gemiddelde tijd van O(n+m).<br />

Hoe vind<strong>en</strong> we e<strong>en</strong> functie die voldoet aan <strong>voor</strong>waard<strong>en</strong> (a), (b) <strong>en</strong> (c) <strong>voor</strong> q > m? Naar analogie<br />

met de pariteitscontrole, is e<strong>en</strong> eerste idee het bepal<strong>en</strong> van de som van de m bits. Deze functie<br />

voldoet aan <strong>voor</strong>waard<strong>en</strong> (a) <strong>en</strong> (c), maar niet aan <strong>voor</strong>waarde (b), want er is bv. maar één string<br />

die als waarde 0 of m geeft.<br />

Rabin <strong>en</strong> Karp suggereerd<strong>en</strong> de volg<strong>en</strong>de hashfunctie: beschouw de m bits als de binaire <strong>voor</strong>stelling<br />

van e<strong>en</strong> natuurlijk getal <strong>en</strong> neem de rest bij deling door q. Zij s0s1...sm−1 de m bits, dan<br />

berek<strong>en</strong><strong>en</strong> we de hashwaarde als<br />

m−1<br />

∑ s j2<br />

j=0<br />

m−1− j mod q.<br />

Deze functie voldoet triviaal aan <strong>voor</strong>waarde (a). Voorwaarde (b) hangt af van de waarde van q.<br />

Voor willekeurige patron<strong>en</strong> <strong>en</strong> tekst<strong>en</strong> is de waarde van q weinig belangrijk, maar in de praktijk<br />

moet<strong>en</strong> bepaalde q-waard<strong>en</strong> vermed<strong>en</strong> word<strong>en</strong>. Bij<strong>voor</strong>beeld, veronderstel dat de tekst<strong>en</strong> bestaan<br />

uit ongeveer 30% 1-<strong>en</strong> <strong>en</strong> 70% 0-<strong>en</strong>, <strong>en</strong> dat q = 2 <strong>en</strong> m = 2. Dan verwacht<strong>en</strong> we dat de meeste<br />

sub<strong>strings</strong> van de vorm 00 zull<strong>en</strong> zijn, <strong>en</strong> dat 01, 10 <strong>en</strong> 11 veel minder <strong>voor</strong>kom<strong>en</strong>. Er zijn dus<br />

veel meer sub<strong>strings</strong> met vingerafdruk 0 dan met vingerafdruk 1. In de praktijk blijkt de keuze<br />

van e<strong>en</strong> priemgetal <strong>voor</strong> q > m goed te werk<strong>en</strong>, <strong>en</strong> aan <strong>voor</strong>waarde (b) te voldo<strong>en</strong>.<br />

Vervolg<strong>en</strong>s gaan we na of <strong>voor</strong>waarde (c) voldaan is. Beschouw m+1 bits s0s1...sm−1sm. De<br />

eerste m bits krijg<strong>en</strong> als vingerafdruk a = ∑ m−1<br />

j=0 s j2m−1− j mod q. De bits s1...sm krijg<strong>en</strong> als<br />

vingerafdruk<br />

m−1<br />

∑ s j+12<br />

j=0<br />

m−1− j mod q<br />

m−2<br />

= sm + ∑ s j+12<br />

j=0<br />

m−1− j mod q<br />

m−2<br />

= sm + 2<br />

∑<br />

j=0<br />

s j+12 m−1−( j+1) mod q<br />

m−1<br />

= sm + 2 s j2 m−1− j mod q<br />

= sm + 2<br />

∑<br />

j=1<br />

<br />

−2 m−1 s0 +<br />

= sm + 2(a − 2 m−1 s0)mod q.<br />

<br />

m−1<br />

m−1− j<br />

∑ s j2 mod q<br />

j=0<br />

Gebruik mak<strong>en</strong>d van deze formule kan de volg<strong>en</strong>de vingerafdruk uit de <strong>voor</strong>gaande berek<strong>en</strong>d<br />

word<strong>en</strong> in constante tijd. Merk op dat 2 m−1 best e<strong>en</strong>maal <strong>voor</strong>af kan berek<strong>en</strong>d word<strong>en</strong>, omdat<br />

deze waarde in elke stap gebruikt wordt.<br />

Algoritme 1.2 geeft de pseudocode <strong>voor</strong> het algoritme van Rabin-Karp.<br />

Stelling 1.2.1. Het algoritme van Rabin-Karp vindt de correcte match van e<strong>en</strong> patroon in e<strong>en</strong><br />

tekst (wanneer die bestaat) in uitvoeringstijd O(m(n − m+1)).<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


6 Hoofdstuk 1. <strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> exact string-matching<br />

Algoritme 1.2 (Rabin-Karp) Zoek<strong>en</strong> naar het <strong>voor</strong>kom<strong>en</strong> van e<strong>en</strong> patroon P in e<strong>en</strong> tekst T<br />

Input: patroon P van l<strong>en</strong>gte m, tekst T van l<strong>en</strong>gte n<br />

Output: de kleinste index i zodanig dat T[i..i+m−1] = P, of −1 wanneer P niet optreedt in T<br />

1: Stel q ← e<strong>en</strong> priemgetal > m<br />

2: Stel r ← 2 m−1 mod q<br />

3: Berek<strong>en</strong> fp ← ∑ m−1<br />

j=0 P[ j]2m−1− j mod q<br />

4: Berek<strong>en</strong> ft[0] ← ∑ m−1<br />

j=0 T[ j]2m−1− j mod q<br />

5: Stel i ← 0<br />

6: while i+m ≤ n do<br />

7: if ft[i] = fp th<strong>en</strong><br />

8: if T[i..i+m−1] = P th<strong>en</strong><br />

9: return i<br />

10: Stel ft[i+1] ← 2 ×(ft[i] − r × T[i])+T[i+m]mod q<br />

11: Stel i ← i+1<br />

12: return −1<br />

1.3 Het algoritme van Knuth-Morris-Pratt<br />

In deze paragraaf besprek<strong>en</strong> we het algoritme van Knuth-Morris-Pratt, dat het string-matchingprobleem<br />

oplost in O(|P|+|T|) tijd. Maar eerst bekijk<strong>en</strong> we aan de hand van <strong>en</strong>kele <strong>voor</strong>beeld<strong>en</strong><br />

waarom het e<strong>en</strong>voudige brute-kracht-algoritme inefficiënt kan zijn.<br />

1.3.1 Inefficiëntie van het brute-kracht-algoritme<br />

Voorbeeld 1.3.1. Veronderstel dat we zoek<strong>en</strong> naar e<strong>en</strong> patroon P = tweedledum in e<strong>en</strong> tekst<br />

T = tweedledee and tweedledum <strong>en</strong> dat we de volg<strong>en</strong>de situatie bereikt hebb<strong>en</strong>:<br />

tweedledee and tweedledum<br />

tweedledu<br />

Op dit mom<strong>en</strong>t zoud<strong>en</strong> we het patroon P één plaats opschuiv<strong>en</strong> teg<strong>en</strong>over de tekst <strong>en</strong> opnieuw<br />

beginn<strong>en</strong> controler<strong>en</strong>. We kunn<strong>en</strong> echter reeds wet<strong>en</strong> dat tweedledum niet op de tweede positie<br />

kan <strong>voor</strong>kom<strong>en</strong>, omdat de tekst begint met tweedled. In het bijzonder bevat tweedled ge<strong>en</strong><br />

<strong>en</strong>kele t na de eerste, zodat we het patroon in de tekst kunn<strong>en</strong> opschuiv<strong>en</strong> tot de eerste letter na<br />

tweedled in de tekst <strong>en</strong> vanaf daar het controler<strong>en</strong> verderzett<strong>en</strong>:<br />

tweedledee and tweedledum<br />

tweedledu<br />

Voorbeeld 1.3.2. Als tweede <strong>voor</strong>beeld beschouw<strong>en</strong> we het zoek<strong>en</strong> van patroon P = pappar<br />

in tekst T = pappappapparrassanuaragh.<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


1.3. Het algoritme van Knuth-Morris-Pratt 7<br />

pappappapparrassanuaragh<br />

pappar<br />

Na 6 vergelijking<strong>en</strong> bekom<strong>en</strong> we e<strong>en</strong> conflict tuss<strong>en</strong> de r op het einde van P <strong>en</strong> de p op de<br />

corresponder<strong>en</strong>de positie in T . Het e<strong>en</strong>voudige algoritme begint dan met het patroon te match<strong>en</strong><br />

teg<strong>en</strong>over T[1] = a. Maar hier hadd<strong>en</strong> we reeds pappa teg<strong>en</strong>over de tekst gematcht, m.a.w.<br />

T[0..4] = pappa <strong>en</strong> we kunn<strong>en</strong> we het patroon 3 posities <strong>voor</strong>uit schuiv<strong>en</strong>:<br />

pappa...................<br />

pappar<br />

Bov<strong>en</strong>di<strong>en</strong> wet<strong>en</strong> we dat P[0..1] = T[3..4] <strong>en</strong> kunn<strong>en</strong> we dus het zoek<strong>en</strong> verderzett<strong>en</strong> vanaf T[5],<br />

m.a.w. vanaf dezelfde positie in de tekst waar de mismatch optrad:<br />

pappappapparrassanuaragh<br />

pappar<br />

1.3.2 Gebruik van e<strong>en</strong> verschuivingstabel<strong>voor</strong> het patroon<br />

Algeme<strong>en</strong> kunn<strong>en</strong> we, <strong>voor</strong> e<strong>en</strong> gegev<strong>en</strong> patroon P, e<strong>en</strong> tabel opstell<strong>en</strong> die <strong>voor</strong> elke k het antwoord<br />

op de volg<strong>en</strong>de vraag bijhoudt: Wanneer P[0..k] de tekst matcht <strong>en</strong> P[k + 1] niet, over<br />

hoeveel posities kunn<strong>en</strong> we het patroon dan opschuiv<strong>en</strong>? Hoe de tabel opgesteld kan word<strong>en</strong>,<br />

besprek<strong>en</strong> we later.<br />

Voorbeeld 1.3.3. De tabel <strong>voor</strong> P = tweedledum is:<br />

t w e e d l e d u m<br />

k −1 0 1 2 3 4 5 6 7 8 9<br />

S 1 1 2 3 4 5 6 7 8 9 10<br />

Merk op dat in dit geval S[k] = max{1,k + 1}. Het is gemakkelijk in te zi<strong>en</strong> dat, wanneer we<br />

P[0..k] gematcht hebb<strong>en</strong> teg<strong>en</strong>over T[i..i+k], maar P[k+1] = T[i+k+1], er ge<strong>en</strong> match <strong>voor</strong> P<br />

kan zijn start<strong>en</strong>d op posities i t.e.m. i+k in T . Immers, er kan ge<strong>en</strong> match zijn op positie i, want<br />

we hebb<strong>en</strong> net vastgesteld dat P[k+ 1] = T[i+k+ 1]. Bov<strong>en</strong>di<strong>en</strong> zijn er ge<strong>en</strong> matches mogelijk<br />

op posities i+1 t.e.m. i+k, want daar staan dezelfde karakters als P[1..k], <strong>en</strong> dus bevattt<strong>en</strong> ze<br />

ge<strong>en</strong> t, waarmee e<strong>en</strong> match met P moet beginn<strong>en</strong>. We kunn<strong>en</strong> het patroon dus opschuiv<strong>en</strong> tot<br />

positie i+k+ 1 in de tekst <strong>en</strong> beginn<strong>en</strong> met het vergelijk<strong>en</strong> van P[0] <strong>en</strong> T[i+k+ 1]. In het geval<br />

dat k = −1 wet<strong>en</strong> we dat P[0] = T[1], zodat we het patroon naar de volg<strong>en</strong>de positie opschuiv<strong>en</strong>.<br />

Wanneer dus het zoek<strong>en</strong> van tweedledum in tweedledee and tweedledum faalt op<br />

P[8], dan gebruik<strong>en</strong> we S[7] om te bepal<strong>en</strong> dat we het patroon over 8 posities mog<strong>en</strong> verschuiv<strong>en</strong>.<br />

De volg<strong>en</strong>de vergelijking is P[0] = t teg<strong>en</strong>over T[8] = e.<br />

Voorbeeld 1.3.4. De tabel <strong>voor</strong> P = pappar is:<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


8 Hoofdstuk 1. <strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> exact string-matching<br />

p a p p a r<br />

k −1 0 1 2 3 4 5<br />

S 1 1 2 2 3 3 6<br />

Veronderstel dat we het patroon P = pappar zoek<strong>en</strong> in de tekst T = panther. Bij de mismatch<br />

van P[2] teg<strong>en</strong>over de n in panther kunn<strong>en</strong> we patroon verschuiv<strong>en</strong> over S[1] = 2 posities <strong>en</strong><br />

verdergaan met het vergelijk<strong>en</strong> van P[0] = p met T[2] = n:<br />

panther panther<br />

pappar pappar<br />

- -<br />

Bij het zoek<strong>en</strong> van P = pappar in T = papaya tree, kunn<strong>en</strong> we bij de mismatch van P[3]<br />

het patroon verschuiv<strong>en</strong> over S[2] = 2 posities, <strong>en</strong> verdergaan met het vergelijk<strong>en</strong> van P[1] = a<br />

met T[3] = a (d.i. de positie in de tekst waar de mismatch optrad):<br />

papaya tree papaya tree<br />

pappar pappar<br />

- -<br />

Bekijk<strong>en</strong> we t<strong>en</strong>slotte het zoek<strong>en</strong> van P = pappar in de tekst T = pappappapparrassanua:<br />

pappappapparrassanua<br />

pappar<br />

-<br />

Na 6 vergelijking<strong>en</strong> wet<strong>en</strong> we dat P[0..4] = T[0..4] <strong>en</strong> P[5] = T[5]. We verschuiv<strong>en</strong> het patroon<br />

over S[4] = 3 posities <strong>en</strong> vervolg<strong>en</strong> met het vergelijk<strong>en</strong> van P[2] met T[5]:<br />

pappappapparrassanua<br />

pappar<br />

-<br />

Na nog 4 vergelijking<strong>en</strong> vind<strong>en</strong> we e<strong>en</strong> mismatch P[5] = T[8], waardoor we het patroon mog<strong>en</strong><br />

verschuiv<strong>en</strong> over S[4] = 3 posities <strong>en</strong> verderzoek<strong>en</strong> in de tekst vanaf T[8], te vergelijk<strong>en</strong> met P[2]:<br />

pappappapparrassanua<br />

pappar<br />

-<br />

Na nog e<strong>en</strong>s 4 vergelijking<strong>en</strong> vind<strong>en</strong> we e<strong>en</strong> match <strong>voor</strong> het patroon in de tekst.<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


1.3. Het algoritme van Knuth-Morris-Pratt 9<br />

Algoritme 1.3 (Knuth-Morris-Pratt) Zoek<strong>en</strong> naar e<strong>en</strong> patroon P in e<strong>en</strong> tekst T<br />

Input: patroon P van l<strong>en</strong>gte m, tekst T van l<strong>en</strong>gte n<br />

Output: de kleinste index i zodanig dat T[i..i+m−1] = P, of −1 wanneer P niet optreedt in T<br />

1: Berek<strong>en</strong> de verschuivingstabel S<br />

2: Stel i ← 0 <strong>en</strong> j ← 0<br />

3: while i+m ≤ n do<br />

4: while T[i+ j] = P[ j] do<br />

5: Stel j ← j+ 1<br />

6: if j ≥ m th<strong>en</strong><br />

7: return i<br />

8: Stel i ← i+S[ j − 1]<br />

9: Stel j ← max(0, j − S[ j − 1])<br />

10: return −1<br />

Algeme<strong>en</strong> kunn<strong>en</strong> we het gebruik van de verschuivingstabel als volgt argum<strong>en</strong>ter<strong>en</strong>. Veronderstel<br />

dat P[0..k], m.a.w. het patroon t.e.m. positie k, de karakters in de tekst start<strong>en</strong>d op positie i<br />

matcht, m.a.w. P[0..k] = T[i..i+k]. We beschouw<strong>en</strong> het verschuiv<strong>en</strong> van het patroon over s posities.<br />

Opdat de verschuiving mogelijk zou zijn, moet<strong>en</strong> de eerste k − s+1 karakters van P de<br />

tekst op de nieuwe positie match<strong>en</strong>:<br />

P: 0 1 2 . . . s s+1 s+2 . . . k . . .<br />

T : 0 . . . i i+1 i+2 . . . i+s i+s+1 i+s+2 . . . i+k . . . i+s+k<br />

P: 0 1 2 . . . k − s . . . k<br />

Uit het bov<strong>en</strong>staande diagram blijkt dat, opdat de verschuiving mogelijk zou zijn, er moet geld<strong>en</strong><br />

dat P[0..k − s] = T[i + s..i+ k]. We wet<strong>en</strong> echter ook dat T[i + s..i+k] = P[s..k]. Dus moet<br />

P[0..k − s] = P[s..k]. Dit is de <strong>voor</strong>waarde opdat e<strong>en</strong> verschuiving mogelijk zou zijn. Om ge<strong>en</strong><br />

mogelijke match te miss<strong>en</strong>, moet<strong>en</strong> we de kleinste van dergelijke verschuiving<strong>en</strong> beschouw<strong>en</strong>.<br />

M.a.w.<br />

S[k] = min{s > 0 | P[0..k − s] = P[s..k]}.<br />

We vereis<strong>en</strong> s > 0 omdat we e<strong>en</strong> effectieve verschuiving will<strong>en</strong> bekom<strong>en</strong>. Het beste geval treedt<br />

op wanneer er ge<strong>en</strong> <strong>en</strong>kele match is <strong>en</strong> s = k + 1 (in dat geval zijn P[0..k − s] <strong>en</strong> P[s..k] beid<strong>en</strong><br />

leeg <strong>en</strong> dus gelijk).<br />

1.3.3 Het algoritme van Knuth-Morris-Pratt<br />

Algoritme 1.3 geeft de pseudocode <strong>voor</strong> het algoritme van Knuth-Morris-Pratt. Veronderstell<strong>en</strong><br />

we <strong>voor</strong>lopig dat het berek<strong>en</strong><strong>en</strong> van de verschuivingstabel correct gebeurt in tijd O(m) (we<br />

besprek<strong>en</strong> later hoe dit kan). Om de correctheid van het algoritme van Knuth-Morris-Pratt te<br />

bewijz<strong>en</strong>, moet<strong>en</strong> we aanton<strong>en</strong> dat bov<strong>en</strong>staande intuïtieve beschrijving van het gebruik van de<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


10 Hoofdstuk 1. <strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> exact string-matching<br />

verschuivingstabel correct is, m.a.w. dat de verschuivingstabel ons de volg<strong>en</strong>de mogelijke positie<br />

<strong>voor</strong> e<strong>en</strong> match geeft, zonder tuss<strong>en</strong>ligg<strong>en</strong>de matches over te slaan. Dit wordt bewez<strong>en</strong> in de<br />

volg<strong>en</strong>de stelling.<br />

Stelling 1.3.5. Veronderstel dat P[0.. j − 1] = T[i..i+ j − 1] <strong>en</strong> P[ j] = T[i+ j], m.a.w. we hebb<strong>en</strong><br />

e<strong>en</strong> gedeeltelijke match van patroon P die op positie i + j in T faalt. Zij i ′ = i + S[ j − 1] <strong>en</strong><br />

j ′ = max{0, j − S[ j − 1]}. Dan geldt:<br />

(a) P[0.. j ′ − 1] = T[i ′ ..i ′ + j ′ − 1], <strong>en</strong><br />

(b) P = T[k..k+ m − 1] <strong>voor</strong> alle i ≤ k < i ′ .<br />

M.a.w. P[0.. j ′ − 1] matcht T start<strong>en</strong>d op positie i ′ , <strong>en</strong> er zijn ge<strong>en</strong> matches <strong>voor</strong> P op posities<br />

i,i+1,...,i ′ − 1.<br />

Bewijs. Bij definitie is S[ j − 1] e<strong>en</strong> toegelat<strong>en</strong> verschuiving, nl.<br />

Dus<br />

P[0.. j − 1 − S[ j − 1]] = P[S[ j − 1].. j − 1].<br />

P[0.. j ′ − 1] = P[0.. j − 1 − S[ j − 1]] per definitie van j ′<br />

= P[S[ j − 1].. j − 1] per definitie van S[ j − 1]<br />

= T[i+S[ j − 1]..i+ j − 1] want P[0.. j − 1] = T[i..i+ j − 1]<br />

= T[i ′ ..i ′ + j ′ − 1] per definitie van i ′ <strong>en</strong> j ′<br />

Dit bewijst (a). Om (b) te bewijz<strong>en</strong> moet<strong>en</strong> we aanton<strong>en</strong> dat S[ j − 1] de kleinste toegelat<strong>en</strong><br />

verschuiving is <strong>en</strong> dat we ge<strong>en</strong> mogelijke matches miss<strong>en</strong> door de verschuiving uit te voer<strong>en</strong>.<br />

We bewijz<strong>en</strong> dit uit het ongerijmde. Onderstel dat P[0..m − 1] = T[k..k + m − 1] <strong>voor</strong> zekere<br />

i ≤ k < i ′ . Stel k ′ = k − i. Dan<br />

P[0.. j − 1 − k ′ ] = T[k..k+ j − 1 − k ′ ] bij veronderstelling<br />

= T[k..i+ j − 1] per definitie van k ′<br />

= P[k ′ .. j − 1] want P[0.. j − 1] = T[i..i+ j − 1]<br />

Dus is k ′ ≥ S[ j − 1], weg<strong>en</strong>s definitie van S[ j − 1], hetge<strong>en</strong> strijdig is met de veronderstelling dat<br />

k ′ = k − i < i ′ − i = S[ j − 1].<br />

Stelling 1.3.6. De uitvoeringstijd van het algoritme van Knuth-Morris-Pratt is Θ(m+n).<br />

Bewijs. De buit<strong>en</strong>ste lus (op lijn 3) wordt hoogst<strong>en</strong>s n keer uitgevoerd, want i wordt in elke iteratie<br />

verhoogd, omdat de verschuivingstabel <strong>en</strong>kel strikt positieve waard<strong>en</strong> bevat. De binn<strong>en</strong>ste<br />

lus (op lijn 4) zou echter m stapp<strong>en</strong> kunn<strong>en</strong> hebb<strong>en</strong>, hetge<strong>en</strong> zou leid<strong>en</strong> tot e<strong>en</strong> bov<strong>en</strong>gr<strong>en</strong>s op de<br />

uitvoeringstijd van O(nm), hetge<strong>en</strong> niet aanvaardbaar is.<br />

Om het totale aantal stapp<strong>en</strong> in beide luss<strong>en</strong> beter in te schatt<strong>en</strong>, beschouw<strong>en</strong> we de evolutie<br />

van de waarde 2i+ j tijd<strong>en</strong>s de uitvoering van lijn 4 van het algoritme. Wanneer de vergelijking<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


1.3. Het algoritme van Knuth-Morris-Pratt 11<br />

Algoritme 1.4 (Knuth-Morris-Pratt verschuivingstabel) Opbouw<strong>en</strong> van de verschuivingstabel<br />

<strong>voor</strong> het algoritme van Knuth-Morris-Pratt<br />

Input: patroon P van l<strong>en</strong>gte m<br />

Output: de verschuivingstabel S<br />

1: Stel S[−1] ← 1<br />

2: Stel S[0] ← 1<br />

3: Stel i ← 1 <strong>en</strong> j ← 0<br />

4: while i+ j < m do<br />

5: if P[i+ j] = P[ j] th<strong>en</strong><br />

6: Stel S[i+ j] ← i<br />

7: Stel j ← j+ 1<br />

8: else<br />

9: if j = 0 th<strong>en</strong><br />

10: Stel S[i] ← i+1<br />

11: Stel i ← i+S[ j − 1]<br />

12: Stel j ← max(0, j − S[ j − 1])<br />

T[i+ j] = P[ j] true levert, wordt j met 1 verhoogd, <strong>en</strong> dus ook 2i+ j. Wanneer de vergelijking<br />

false levert, wordt S[ j − 1] opgeteld bij i <strong>en</strong> hoogst<strong>en</strong>s S[ j − 1] afgetrokk<strong>en</strong> van j, m.a.w. 2i+ j<br />

verhoogt met minst<strong>en</strong>s S[ j − 1] ≥ 1. Dus elke keer wanneer het algoritme terugkeert naar de test<br />

op lijn 4 is de waarde van 2i+ j verhoogd. Aangezi<strong>en</strong> 2i+ j ≤ 2n+m, kan dit hoogst<strong>en</strong>s 2n+m<br />

keer gebeur<strong>en</strong>, hetge<strong>en</strong> e<strong>en</strong> bov<strong>en</strong>gr<strong>en</strong>s van O(n+m) <strong>voor</strong> de lus levert.<br />

Aangezi<strong>en</strong> we veronderstell<strong>en</strong> dat de initialisatie tijd O(m) vraagt, levert dit dus e<strong>en</strong> totale uitvoeringstijd<br />

<strong>voor</strong> het algoritme van O(n+m).<br />

1.3.4 Berek<strong>en</strong><strong>en</strong> van de verschuivingstabel<br />

T<strong>en</strong>slotte bekijk<strong>en</strong> we hoe de verschuivingstabel kan opgesteld word<strong>en</strong> in O(m) tijd. Het idee is<br />

hetzelfde als <strong>voor</strong> het zoek<strong>en</strong> van e<strong>en</strong> patroon in e<strong>en</strong> tekst, met als verschil dat we nu het patroon<br />

in zichzelf prober<strong>en</strong> te lokaliser<strong>en</strong>.<br />

Veronderstel dat we algoritme 1.3 uitvoer<strong>en</strong> met T = P. Telk<strong>en</strong>s wanneer we j in de binn<strong>en</strong>ste lus<br />

increm<strong>en</strong>ter<strong>en</strong> hebb<strong>en</strong> we e<strong>en</strong> gedeeltelijke match P[0.. j] = P[i..i+ j] gevond<strong>en</strong>. Dit impliceert<br />

dat S[i+ j] ≤ i, want<br />

S[i+ j] = min{s > 0 | P[0..i+ j − s] = P[s..i+ j]}.<br />

Anderzijds wet<strong>en</strong> we ook dat deze manier van werk<strong>en</strong> gegarandeerd ge<strong>en</strong> matches mist, zodat<br />

we kunn<strong>en</strong> stell<strong>en</strong> dat S[i+ j] = i <strong>en</strong> we kunn<strong>en</strong> deze waarde in de tabel invull<strong>en</strong>.<br />

Algoritme 1.4 implem<strong>en</strong>teert dit idee. Merk op dat tijd<strong>en</strong>s het algoritme <strong>en</strong>kel waard<strong>en</strong> uit de<br />

verschuivingstabel gebruikt word<strong>en</strong> die reeds eerder ingevuld werd<strong>en</strong>.<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


12 Hoofdstuk 1. <strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> exact string-matching<br />

Algoritme 1.5 (Boyer-Moore) Zoek<strong>en</strong> naar e<strong>en</strong> patroon P in e<strong>en</strong> tekst T (e<strong>en</strong>voudige versie)<br />

Input: patroon P van l<strong>en</strong>gte m, tekst T van l<strong>en</strong>gte n<br />

Output: de kleinste index i zodanig dat T[i..i+m−1] = P, of −1 wanneer P niet optreedt in T<br />

1: Stel i ← 0<br />

2: while i+m ≤ n do<br />

3: Stel j ← m − 1<br />

4: while T[i+ j] = P[ j] do<br />

5: Stel j ← j − 1<br />

6: if j < 0 th<strong>en</strong><br />

7: return i<br />

8: Stel i ← i+1<br />

9: return −1<br />

1.4 Het algoritme van Boyer-Moore-Horspool<br />

Boyer <strong>en</strong> Moore kwam<strong>en</strong> op het volg<strong>en</strong>de schijnbaar onschuldige idee: waarom niet prober<strong>en</strong><br />

om het patroon van rechts naar links met de tekst te vergelijk<strong>en</strong> in plaats van van links naar<br />

rechts, maar wel de richting van de verschuiving behoud<strong>en</strong>? Bij<strong>voor</strong>beeld, e<strong>en</strong> herimplem<strong>en</strong>tatie<br />

van het e<strong>en</strong>voudige Algoritme 1.1 levert Algoritme 1.5.<br />

Dit algoritme vereist ook O(mn) uitvoeringstijd, maar het is wel zeer geschikt <strong>voor</strong> het implem<strong>en</strong>ter<strong>en</strong><br />

van twee heuristiek<strong>en</strong> die het algoritme significant zull<strong>en</strong> versnell<strong>en</strong>.<br />

1.4.1 De occurr<strong>en</strong>ce-heuristiek<br />

We bekijk<strong>en</strong> <strong>en</strong>kele <strong>voor</strong>beeld<strong>en</strong> van de occurr<strong>en</strong>ce-heuristiek.<br />

Voorbeeld 1.4.1. Veronderstel dat we het patroon rum zoek<strong>en</strong> in de tekst conumdrum. Met de<br />

Boyer-Moore-versie van het e<strong>en</strong>voudige algoritme zull<strong>en</strong> we eerst de m van rum vergelijk<strong>en</strong> met<br />

de n van conumdrum:<br />

conumdrum<br />

rum<br />

Deze vergelijking faalt uiteraard, <strong>en</strong> bov<strong>en</strong>di<strong>en</strong> wet<strong>en</strong> we dat het patroon rum de letter n niet<br />

bevat, zodat we het patroon kunn<strong>en</strong> verschuiv<strong>en</strong> over 3 posities:<br />

conumdrum<br />

rum<br />

Hier zi<strong>en</strong> we dat m niet matcht met d, <strong>en</strong> we wet<strong>en</strong> dat het patroon ge<strong>en</strong> d bevat, zodat we de het<br />

patroon weer over 3 posities kunn<strong>en</strong> verschuiv<strong>en</strong>:<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


1.4. Het algoritme van Boyer-Moore-Horspool 13<br />

conumdrum<br />

rum<br />

Hier vind<strong>en</strong> we uiteindelijk e<strong>en</strong> match.<br />

Voorbeeld 1.4.2. Als tweede <strong>voor</strong>beeld zoek<strong>en</strong> we naar het patroon drum in de tekst conundrum:<br />

conundrum<br />

drum<br />

Opnieuw hebb<strong>en</strong> we e<strong>en</strong> mismatch, maar dit keer komt de letter u uit de tekst wel in het patroon<br />

<strong>voor</strong>. We kunn<strong>en</strong> dus het patroon over 1 positie verschuiv<strong>en</strong>, namelijk zodanig dat de (meest<br />

rechtse) u uit het patroon drum gealigneerd wordt met de u (op de positie van de mismatch) in<br />

de tekst conundrum:<br />

conundrum<br />

drum<br />

De mismatch tuss<strong>en</strong> m <strong>en</strong> n leidt nu tot het verschuiv<strong>en</strong> over 4 posities:<br />

conundrum<br />

drum<br />

Dit levert de uiteindelijke match.<br />

Voorbeeld 1.4.3. Als laatste <strong>voor</strong>beeld bekijk<strong>en</strong> we het zoek<strong>en</strong> van het patroon natu in de tekst<br />

conundrum:<br />

conundrum<br />

natu<br />

De match van t <strong>en</strong> n faalt, zodat we het patroon verschuiv<strong>en</strong> zodanig dat de n uit patroon <strong>en</strong><br />

tekst elkaar match<strong>en</strong>:<br />

conundrum<br />

natu<br />

Hier faalt de match van t <strong>en</strong> n, <strong>en</strong> natu bevat ge<strong>en</strong> d, zodat we het patroon <strong>voor</strong>bij de d in de<br />

tekst moet<strong>en</strong> schuiv<strong>en</strong>. Maar aangezi<strong>en</strong> er in de tekst slechts drie karakters <strong>voor</strong>bij d overblijv<strong>en</strong>,<br />

wet<strong>en</strong> we dat het patroon niet in de tekst optreedt.<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


14 Hoofdstuk 1. <strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> exact string-matching<br />

We kunn<strong>en</strong> de occurr<strong>en</strong>ce-heuristiek als volgt sam<strong>en</strong>vatt<strong>en</strong>: Vergelijk het patroon met de tekst<br />

van rechts naar links. Wanneer e<strong>en</strong> mismatch van e<strong>en</strong> karakter α uit de tekst optreedt, verschuif<br />

het patroon zodanig dat het meeste rechtse <strong>voor</strong>kom<strong>en</strong> van α in het patroon gealigneerd wordt<br />

op α van de mismatch in de tekst. Indi<strong>en</strong> het patroon ge<strong>en</strong> α bevat, verschuif het patroon dan tot<br />

de positie <strong>voor</strong>bij α in de tekst.<br />

Er is echter e<strong>en</strong> subtiel probleem met deze heuristiek: het zou kunn<strong>en</strong> gebeur<strong>en</strong> dat het patroon<br />

terug naar links verschov<strong>en</strong> wordt in plaats van naar rechts, wanneer het meest rechtse <strong>voor</strong>kom<strong>en</strong><br />

van α reeds <strong>voor</strong>bij de huidige positie geschov<strong>en</strong> is. De oplossing die Boyer <strong>en</strong> Moore<br />

<strong>voor</strong>steld<strong>en</strong>, is om dergelijke gevall<strong>en</strong> het patroon over 1 positie naar rechts te verschuiv<strong>en</strong>.<br />

Voorbeeld 1.4.4. We zoek<strong>en</strong> het patroon date in de tekst detective:<br />

detective<br />

date<br />

We krijg<strong>en</strong> e<strong>en</strong> mismatch bij a <strong>en</strong> e, hetge<strong>en</strong> zou leid<strong>en</strong> tot de volg<strong>en</strong>de verschuiving:<br />

detective<br />

date<br />

Dit zou dus betek<strong>en</strong><strong>en</strong> dat het patroon verschov<strong>en</strong> wordt naar e<strong>en</strong> positie die reeds eerder behandeld<br />

werd, m.a.w. e<strong>en</strong> negatieve verschuiving. We verschuiv<strong>en</strong> het patroon dus over 1 positie<br />

naar rechts:<br />

detective<br />

date<br />

1.4.2 De match-heuristiek<br />

De tweede heuristiek is analoog aan het idee gebruikt in het algoritme van Knuth-Morris-Pratt:<br />

Wanneer e<strong>en</strong> match-proces halverwege faalt, dan kunn<strong>en</strong> we informatie over het stuk tekst dat we<br />

tot dan toe gezi<strong>en</strong> hebb<strong>en</strong>, gebruik<strong>en</strong> om het patroon naar de volg<strong>en</strong>de mogelijke match te schuiv<strong>en</strong>.<br />

Aangezi<strong>en</strong> we echter van rechts naar links vergelijk<strong>en</strong>, moet<strong>en</strong> we de verschuivingstabel<br />

<strong>voor</strong> het achterstevor<strong>en</strong> patroon berek<strong>en</strong><strong>en</strong>.<br />

Voorbeeld 1.4.5. We gebruik<strong>en</strong> de match-heuristiek om het patroon P = banana in de tekst<br />

T = a banana te zoek<strong>en</strong>:<br />

a banana<br />

banana<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


1.4. Het algoritme van Boyer-Moore-Horspool 15<br />

We vind<strong>en</strong> e<strong>en</strong> match <strong>voor</strong> P[3..5] = ana met T[3..5] = ana, <strong>en</strong> vervolg<strong>en</strong>s e<strong>en</strong> mismatch <strong>voor</strong><br />

P[2] = n met T[2] = b. De Knuth-Morris-Pratt-verschuivingstabel <strong>voor</strong> ananab levert S[3] = 2,<br />

zodat we P over twee posities naar rechts kunn<strong>en</strong> verschuiv<strong>en</strong>:<br />

a banana<br />

banana<br />

Op dit punt vind<strong>en</strong> we e<strong>en</strong> match.<br />

1.4.3 Het algoritme van Boyer-Moore-Horspool<br />

Het algoritme van Boyer-Moore implem<strong>en</strong>teert deze beide heuristiek<strong>en</strong>, <strong>en</strong> kiest dieg<strong>en</strong>e die<br />

aanleiding geeft tot de grootste verschuiving. Zodo<strong>en</strong>de wordt het algoritme e<strong>en</strong> van de snelste<br />

algoritm<strong>en</strong> <strong>voor</strong> string-matching, zowel in theorie als in de praktijk. De match-heuristiek garandeert<br />

e<strong>en</strong> uitvoeringstijd van O(m+n), zoals in het Knuth-Morris-Pratt-algoritme, <strong>en</strong> er zijn<br />

heel wat verbetering<strong>en</strong> om het effectieve aantal vergelijking<strong>en</strong> te verminder<strong>en</strong>. Het is echter e<strong>en</strong><br />

behoorlijk complex algoritme om te implem<strong>en</strong>ter<strong>en</strong>, <strong>voor</strong>al door de match-heuristiek. Dit is de<br />

red<strong>en</strong> dat we hier e<strong>en</strong> variante beschouw<strong>en</strong> die <strong>voor</strong>gesteld werd door Horspool, <strong>en</strong> die gek<strong>en</strong>d<br />

is als het algoritme van Boyer-Moore-Horspool. Dit algoritme implem<strong>en</strong>teert e<strong>en</strong> gewijzigde<br />

occurr<strong>en</strong>ce-heuristiek <strong>en</strong> is zeer snel in de praktijk, alhoewel zijn slechtste-geval-uitvoeringstijd<br />

O(mn) is.<br />

We hebb<strong>en</strong> reeds eerder e<strong>en</strong> subtiel probleem vermeld dat kon optred<strong>en</strong> bij de occurr<strong>en</strong>ceheuristiek,<br />

nl. dat het tot negatieve verschuiving<strong>en</strong> (verschuiving<strong>en</strong> naar links) kan leid<strong>en</strong> wanneer<br />

het meest rechtse <strong>voor</strong>kom<strong>en</strong> van het karakter reeds rechts van de huidige positie is. Merk<br />

op dat dit geval zich <strong>en</strong>kel <strong>voor</strong>doet wanneer we reeds e<strong>en</strong> gedeelte van het patroon gematcht<br />

hebb<strong>en</strong>. Beschouw het geval waarbij het laatste karakter e<strong>en</strong> mismatch geeft; in dat geval verschuiv<strong>en</strong><br />

we altijd naar rechts. Horspool stelde <strong>voor</strong> om de verschuiving steeds te bepal<strong>en</strong> uit het<br />

laatste karakter, <strong>en</strong> niet uit het karakter dat zorgt <strong>voor</strong> de mismatch. Hierdoor word<strong>en</strong> negatieve<br />

verschuiving<strong>en</strong> vermed<strong>en</strong>.<br />

Voorbeeld 1.4.6. In het geval van<br />

detective<br />

date<br />

faalt de match bij P[1] = a <strong>en</strong> T[1] = e. We verschuiv<strong>en</strong> dan het patroon zodanig dat de laatste<br />

letter T[3] gealigneerd wordt met de volg<strong>en</strong>de mogelijke match in P. Aangezi<strong>en</strong> date ge<strong>en</strong><br />

andere e bevat, kunn<strong>en</strong> we het volledige patroon opschuiv<strong>en</strong> tot <strong>voor</strong>bij T[3] = e:<br />

detective<br />

date<br />

Hierdoor word<strong>en</strong> drie posities overgeslag<strong>en</strong>.<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


16 Hoofdstuk 1. <strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> exact string-matching<br />

Sam<strong>en</strong>vatt<strong>en</strong>d, de reactie van het algoritme van Horspool op e<strong>en</strong> mismatch bestaat erin te prober<strong>en</strong><br />

T[i − m+1] te match<strong>en</strong> met het meest rechtse optred<strong>en</strong> van dat karakter in het patroon links<br />

van P[m − 1]. Deze regel garandeert dat het patroon altijd naar rechts verschov<strong>en</strong> wordt. Om<br />

de verschuiving correct uit te voer<strong>en</strong>, moet<strong>en</strong> we <strong>voor</strong> elk karakter uit het alfabet wet<strong>en</strong> wat zijn<br />

meest rechtse <strong>voor</strong>kom<strong>en</strong> in het patroon links van P[m − 1] is. Meer formeel:<br />

<br />

m − 1 − max{i < m − 1 | P[i] = x} als x behoort tot P[0..m − 2]<br />

S[x] =<br />

m anders<br />

Voorbeeld 1.4.7. Voor het woord P = kettle krijg<strong>en</strong> we de volg<strong>en</strong>de verschuiving<strong>en</strong>:<br />

S[k] = 5; S[e] = 4; S[t] = 2; S[l] = 1.<br />

Voor elke andere letter is de waarde in de verschuivingstabel 6.<br />

Beschouw bij<strong>voor</strong>beeld de tekst T = tea kettle:<br />

tea kettle<br />

kettle<br />

De mismatch van P[4] = l <strong>en</strong> T[4] = k, leidt tot e<strong>en</strong> verschuiving van S[T[5]] = S[e] = 4, <strong>en</strong> we<br />

vind<strong>en</strong> onmiddellijk de match:<br />

tea kettle<br />

kettle<br />

Beschouw bij<strong>voor</strong>beeld de tekst T = a kettle:<br />

a kettle<br />

kettle<br />

De mismatch van P[5] = e <strong>en</strong> T[5] = t leidt tot e<strong>en</strong> verschuiving van S[T[5]] = S[t] = 2:<br />

a kettle<br />

kettle<br />

Opnieuw vind<strong>en</strong> we onmiddellijk e<strong>en</strong> match.<br />

Algoritme 1.6 geeft de pseudocode <strong>voor</strong> het algoritme van Boyer-Moore-Horspool.<br />

Wanneer we veronderstell<strong>en</strong> dat het alfabet Σ vast is, dan loopt het algoritme van Boyer-Moore-<br />

Horspool in O(mn) tijd. In teg<strong>en</strong>stelling tot het volledige Boyer-Moore-algoritme <strong>en</strong> het algoritme<br />

van Knuth-Morris-Pratt hangt de gebruikte geheug<strong>en</strong>ruimte van dit algoritme niet af van P.<br />

Inderdaad, de geheug<strong>en</strong>complexiteit is O(|Σ|), m.a.w. constant indi<strong>en</strong> we veronderstell<strong>en</strong> dat<br />

e<strong>en</strong> vast alfabet gebruikt wordt. In de praktijk zijn de uitvoeringstijd<strong>en</strong> van dit algoritme vergelijkbaar<br />

met die van het algoritme van Boyer-Moore. Hierdoor is het algoritme zeer bruikbaar,<br />

omdat het veel e<strong>en</strong>voudiger te implem<strong>en</strong>ter<strong>en</strong> is <strong>en</strong> minder geheug<strong>en</strong>ruimte gebruikt.<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


1.4. Het algoritme van Boyer-Moore-Horspool 17<br />

Algoritme 1.6 (Boyer-Moore-Horspool) Zoek<strong>en</strong> naar e<strong>en</strong> patroon P in e<strong>en</strong> tekst T over e<strong>en</strong><br />

alfabet Σ<br />

Input: patroon P van l<strong>en</strong>gte m, tekst T van l<strong>en</strong>gte n, alfabet Σ<br />

Output: de kleinste index i zodanig dat T[i..i+m−1] = P, of −1 wanneer P niet optreedt in T<br />

1: {Berek<strong>en</strong><strong>en</strong> van de verschuivingstabel}<br />

2: for k from 0 to |Σ| − 1 do<br />

3: Stel S[k] ← m<br />

4: for k from 0 to m − 2 do<br />

5: Stel S[P[k]] ← m − 1 − k<br />

6: {Zoek<strong>en</strong>}<br />

7: Stel i ← 0<br />

8: while i+m ≤ n do<br />

9: Stel j ← m − 1<br />

10: while T[i+ j] = P[ j] do<br />

11: Stel j ← j − 1<br />

12: if j < 0 th<strong>en</strong><br />

13: return i<br />

14: Stel i ← i+S[T[i+m−1]]<br />

15: return −1<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


18 Hoofdstuk 1. <strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> exact string-matching<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


Hoofdstuk 2<br />

<strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> b<strong>en</strong>ader<strong>en</strong>d<br />

string-matching<br />

Door minder str<strong>en</strong>ge eis<strong>en</strong> te stell<strong>en</strong>, ontstaan e<strong>en</strong> aantal variant<strong>en</strong> van het klassieke probleem<br />

van string-matching. M<strong>en</strong> kan bij<strong>voor</strong>beeld matches toelat<strong>en</strong> die slechts b<strong>en</strong>ader<strong>en</strong>d zijn, waarbij<br />

natuurlijk moet gespecificeerd word<strong>en</strong> wat “b<strong>en</strong>ader<strong>en</strong>d” precies inhoudt. In dit hoofdstuk<br />

bestuder<strong>en</strong> we <strong>voor</strong>beeld<strong>en</strong> hiervan.<br />

Er zijn heel wat variant<strong>en</strong> van het probleem van b<strong>en</strong>ader<strong>en</strong>d string-matching, hetge<strong>en</strong> de veelheid<br />

van situaties weerspiegelt waarin het zoek<strong>en</strong> van e<strong>en</strong> patroon in e<strong>en</strong> tekst gebaseerd is op<br />

onvolledige <strong>en</strong>/of imprecieze informatie.<br />

Beschouw bij<strong>voor</strong>beeld het vergelijk<strong>en</strong> van twee bestand<strong>en</strong> op de computer om uit te vind<strong>en</strong> in<br />

hoeverre ze verschill<strong>en</strong> of wat er veranderd is in e<strong>en</strong> nieuwe versie van e<strong>en</strong> bestand. Dit is het<br />

soort bewerking uitgevoerd door het commando diff in Unix. Hoe kan diff nu wet<strong>en</strong> of hij<br />

de juiste stukk<strong>en</strong> tekst met elkaar vergelijkt, wanneer er meerdere mogelijkhed<strong>en</strong> zijn? Uiteraard<br />

kan het commando dit niet wet<strong>en</strong>, aangezi<strong>en</strong> het niet weet wat het verband tuss<strong>en</strong> de bestand<strong>en</strong><br />

is. E<strong>en</strong> manier om dit probleem op te loss<strong>en</strong> is door het definiër<strong>en</strong> van e<strong>en</strong> maat van verschil<br />

tuss<strong>en</strong> twee bestand<strong>en</strong> als het aantal wijziging<strong>en</strong> dat iemand zou moet<strong>en</strong> aanbr<strong>en</strong>g<strong>en</strong> in het <strong>en</strong>e<br />

bestand om het andere bestand te bekom<strong>en</strong>. Het kleinste aantal nodige wijziging<strong>en</strong> wordt de<br />

editeerafstand tuss<strong>en</strong> de twee bestand<strong>en</strong> g<strong>en</strong>oemd. Hiermee wordt ook e<strong>en</strong> reeks bewerking<strong>en</strong><br />

geassocieerd die het <strong>en</strong>e bestand in het andere omzett<strong>en</strong>.<br />

E<strong>en</strong> ander <strong>voor</strong>beeld is het zoek<strong>en</strong> naar e<strong>en</strong> woord in e<strong>en</strong> tekst, waarbij we echter niet helemaal<br />

zeker zijn dat het woord correct gespeld is. In dit geval will<strong>en</strong> we het zoekprogramma vertell<strong>en</strong><br />

dat het moet zoek<strong>en</strong> naar tekst die sterk gelijkt op het patroon dat we ingev<strong>en</strong>. Het blijkt dat dit<br />

probleem nauw verwant is met het bov<strong>en</strong>staande <strong>voor</strong>beeld van het vergelijk<strong>en</strong> van twee tekst<strong>en</strong>.<br />

19


20 Hoofdstuk 2. <strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> b<strong>en</strong>ader<strong>en</strong>d string-matching<br />

2.1 Editeerafstand tuss<strong>en</strong> <strong>strings</strong><br />

De editeerafstand d(S,T) tuss<strong>en</strong> twee <strong>strings</strong> S <strong>en</strong> T wordt gedefinieerd als het kleinste aantal<br />

editeerbewerking<strong>en</strong> dat nodig is om S in T om te zett<strong>en</strong>. Als toegelat<strong>en</strong> editeerbewerking<strong>en</strong><br />

word<strong>en</strong> meestal de volg<strong>en</strong>de bewerking<strong>en</strong> beschouwd, elk met e<strong>en</strong>heidskost:<br />

• het vervang<strong>en</strong> van e<strong>en</strong> karakter door e<strong>en</strong> ander karakter;<br />

• het verwijder<strong>en</strong> van e<strong>en</strong> karakter;<br />

• het tuss<strong>en</strong>voeg<strong>en</strong> van e<strong>en</strong> karakter.<br />

Voorbeeld 2.1.1. Beschouw het <strong>voor</strong>beeld van het omzett<strong>en</strong> van het woord ghost in het woord<br />

house:<br />

g h o s t verwijder g op positie 0<br />

h o s t voeg u toe na positie 1<br />

h o u s t vervang t door e op positie 4<br />

Merk op dat we niet vereis<strong>en</strong> dat de tuss<strong>en</strong>ligg<strong>en</strong>de woord<strong>en</strong> correcte Nederlandse of Engelse<br />

woord<strong>en</strong> zijn. De kost van deze reeks bewerking<strong>en</strong> is 3.<br />

Merk op dat e<strong>en</strong> woord S steeds in om het ev<strong>en</strong> welk ander woord T kan omgezet word<strong>en</strong>. In<br />

het slechtste geval verwijder<strong>en</strong> we alle letters van S <strong>en</strong> voeg<strong>en</strong> vervolg<strong>en</strong>s alle letters van T toe.<br />

M.a.w.<br />

d(S,T) ≤ |S|+|T|.<br />

Het berek<strong>en</strong><strong>en</strong> van de afstand tuss<strong>en</strong> twee <strong>strings</strong> kan op natuurlijke wijze gebeur<strong>en</strong> m.b.v. de<br />

techniek van dynamisch programmer<strong>en</strong> (zie cursus “<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> II”).<br />

Als deelproblem<strong>en</strong> bij het berek<strong>en</strong><strong>en</strong> van de afstand d(S,T) berek<strong>en</strong><strong>en</strong> we eerst de afstand<strong>en</strong><br />

van de prefix<strong>en</strong> van S <strong>en</strong> T , m.a.w. d(S[0..i],T[0.. j]), <strong>voor</strong> alle 0 ≤ i < |S| <strong>en</strong> alle 0 ≤ j < |T|.<br />

Definiër<strong>en</strong> we als di, j = d(S[0..i],T[0.. j]). De afstand tuss<strong>en</strong> S <strong>en</strong> T is dan d(S,T) = d |S|−1,|T|−1.<br />

Voorbeeld 2.1.2. Hierna volgt de tabel van afstand<strong>en</strong> <strong>voor</strong> de woord<strong>en</strong> S = presto <strong>en</strong> T =<br />

peseta:<br />

0 1 2 3 4 5<br />

p e s e t a<br />

0 p 0 1 2 3 4 5<br />

1 r 1 1 2 3 4 5<br />

2 e 2 1 2 2 3 4<br />

3 s 3 2 1 2 3 4<br />

4 t 4 3 2 2 2 3<br />

5 o 5 4 3 3 3 3<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


2.1. Editeerafstand tuss<strong>en</strong> <strong>strings</strong> 21<br />

Bij<strong>voor</strong>beeld, d(S[0],T[0]) = 0, omdat ze beid<strong>en</strong> dezelfde letter zijn, nl. p. Dan is d(S[0],T[0.. j]) =<br />

j, omdat de snelste manier om p om te zett<strong>en</strong> in e<strong>en</strong> prefix T[0.. j] van peseta erin bestaat<br />

de j letters van T[1.. j] toe te voeg<strong>en</strong>. Op analoge manier kunn<strong>en</strong> we argum<strong>en</strong>ter<strong>en</strong> dat<br />

d(S[0..1],T[0.. j]) = j, <strong>voor</strong> alle j ≥ 1, omdat we r kunn<strong>en</strong> vervang<strong>en</strong> door e, <strong>en</strong> vervolg<strong>en</strong>s de<br />

letters van T[2.. j] toevoeg<strong>en</strong>. Merk op dat d(S[0..1],T[0]) = 1, omdat de snelste manier om pr<br />

om te zett<strong>en</strong> in p bestaat uit het verwijder<strong>en</strong> van r.<br />

Hoe kunn<strong>en</strong> we nu algeme<strong>en</strong> de afstand<strong>en</strong> tuss<strong>en</strong> S[0..i] <strong>en</strong> T[0.. j] berek<strong>en</strong><strong>en</strong>? We beginn<strong>en</strong> met<br />

het einde van de deel<strong>strings</strong> te bekijk<strong>en</strong>: hoe is S[i], de laatste letter van S[0..i], tot T[ j], de laatste<br />

letter van T[0.. j] geword<strong>en</strong>? Er zijn drie mogelijke operaties die hiertoe kunn<strong>en</strong> geleid hebb<strong>en</strong>:<br />

• Vervang<strong>en</strong> van S[i] door T[ j], <strong>en</strong> omzett<strong>en</strong> van S[0..i − 1] tot T[0.. j − 1]. Dit vraagt hoogst<strong>en</strong>s<br />

1+di−1, j−1 bewerking<strong>en</strong>. Immers, wanneer S[i] = T[ j] dan is ge<strong>en</strong> effectieve vervanging<br />

nodig <strong>en</strong> gebeur<strong>en</strong> er slechts di−1, j−1 bewerking<strong>en</strong>.<br />

• Verwijder<strong>en</strong> van S[i], <strong>en</strong> omzett<strong>en</strong> van S[0..i − 1] tot T[0.. j]. Dit vraagt 1+di−1, j bewerking<strong>en</strong>.<br />

• Toevoeg<strong>en</strong> van T[ j] aan het einde van S[0..i], <strong>en</strong> omzett<strong>en</strong> van S[0..i] tot T[0.. j − 1]. Dit<br />

vraagt 1+di, j−1 bewerking<strong>en</strong>.<br />

Hieruit volgt dat we S[0..i] kunn<strong>en</strong> omzett<strong>en</strong> in T[0.. j] in<br />

⎧ <br />

⎪⎨<br />

0 als S[i] = T[ j]<br />

di−1, j−1 +<br />

1 anders<br />

min<br />

⎪⎩<br />

di, j−1 + 1<br />

di−1, j + 1<br />

stapp<strong>en</strong>. Aangezi<strong>en</strong> we alle mogelijkhed<strong>en</strong> beschouwd hebb<strong>en</strong> waarop S[i] in T[ j] kan omgezet<br />

word<strong>en</strong>, geeft dit ons de correcte waarde <strong>voor</strong> di, j. Uit de formule blijkt ook dat di, j kan berek<strong>en</strong>d<br />

word<strong>en</strong> uit de waard<strong>en</strong> van di−1, j−1,di, j−1,di−1, j, m.a.w. uit de tabelwaard<strong>en</strong> in naburige<br />

plaats<strong>en</strong>: linksbov<strong>en</strong> (i − 1, j − 1), links (i, j − 1) <strong>en</strong> bov<strong>en</strong> (i − 1, j). De e<strong>en</strong>voudigste manier<br />

om dit te bekom<strong>en</strong> is door de tabel rij per rij te berek<strong>en</strong><strong>en</strong>, start<strong>en</strong>d in de linkerbov<strong>en</strong>hoek.<br />

Voorbeeld 2.1.3. Beschouw<strong>en</strong> we bij<strong>voor</strong>beeld de berek<strong>en</strong>ing van d3,2 in de bov<strong>en</strong>staande tabel<br />

<strong>voor</strong> S = presto <strong>en</strong> T = peseta. Aangezi<strong>en</strong> S[3] = T[2] is d3,2 het minimum van d2,1 = 1,<br />

d2,2 + 1 = 3 <strong>en</strong> d3,1 + 1 = 3. Dus d3,2 = 1. Beschouw<strong>en</strong> we vervolg<strong>en</strong>s de berek<strong>en</strong>ing van d5,3.<br />

Omdat S[5] = T[3] is d5,3 het minimum van d4,2 + 1 = 3, d4,3 + 1 = 3 <strong>en</strong> d5,2 + 1 = 4. Dus<br />

d5,3 = 3.<br />

Nu we de recursieve formule <strong>voor</strong> het bepal<strong>en</strong> van di, j opgesteld hebb<strong>en</strong>, bekijk<strong>en</strong> we opnieuw<br />

de tabel. Hoe zijn we daar van start gegaan? In rij 0 past<strong>en</strong> we eig<strong>en</strong>lijk de recursieve formule al<br />

toe op e<strong>en</strong> onzichtbare rij −1. Voeg<strong>en</strong> we e<strong>en</strong> rij <strong>en</strong> kolom −1 toe aan de tabel. De waarde van<br />

d−1, j moet het kleinste aantal editeerbewerking<strong>en</strong> zijn dat S[0.. − 1] (d.i. de lege string) omzet<br />

in T[0.. j]. Merk op dat we de lege string in T[0.. j] kunn<strong>en</strong> omzett<strong>en</strong> door de j + 1 tek<strong>en</strong>s van<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


22 Hoofdstuk 2. <strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> b<strong>en</strong>ader<strong>en</strong>d string-matching<br />

Algoritme 2.1 Opstell<strong>en</strong> van de tabel met de editeerafstand<strong>en</strong> tuss<strong>en</strong> de prefix<strong>en</strong> van twee <strong>strings</strong><br />

Input: string S van l<strong>en</strong>gte m, string T van l<strong>en</strong>gte n<br />

Output: d(S,T)<br />

1: {Initialiser<strong>en</strong> van kolom −1}<br />

2: for i from −1 to m − 1 do<br />

3: Stel d[i,−1] ← i+1<br />

4: {Initialiser<strong>en</strong> van rij −1}<br />

5: for j from 0 to n − 1 do<br />

6: Stel d[−1, j] ← j+ 1<br />

7: {Berek<strong>en</strong>ing van de eig<strong>en</strong>lijke tabel}<br />

8: for i from 0 to m − 1 do<br />

9: for j from 0 to n − 1 do<br />

10: if S[i] = T[ j] th<strong>en</strong><br />

11: Stel x ← d[i − 1, j − 1]<br />

12: else<br />

13: Stel x ← 1+d[i − 1, j − 1]<br />

14: Stel d[i, j] ← min(x,1+d[i, j − 1],1+d[i − 1, j])<br />

15: return d[m − 1,n − 1]<br />

T[0.. j] toe te voeg<strong>en</strong>; bov<strong>en</strong>di<strong>en</strong> is gemakkelijk in te zi<strong>en</strong> dat er minst<strong>en</strong>s j + 1 bewerking<strong>en</strong><br />

nodig zijn. Dus d−1, j moet j+ 1 zijn. Analoog moet di,−1 = i+1 zijn.<br />

De tabel <strong>voor</strong> S = presto <strong>en</strong> T = peseta, aangevuld met deze rij <strong>en</strong> kolom, is dan:<br />

−1 0 1 2 3 4 5<br />

p e s e t a<br />

−1 0 1 2 3 4 5 6<br />

0 p 1 0 1 2 3 4 5<br />

1 r 2 1 1 2 3 4 5<br />

2 e 3 2 1 2 2 3 4<br />

3 s 4 3 2 1 2 3 4<br />

4 t 5 4 3 2 2 2 3<br />

5 o 6 5 4 3 3 3 3<br />

Algoritme 2.1 geeft de pseudocode <strong>voor</strong> deze berek<strong>en</strong>ing. De tijdscomplexiteit van dit algoritme<br />

is O(mn).<br />

Merk op dat de tabel berek<strong>en</strong>d in Algoritme 2.1 ons meer dan <strong>en</strong>kel de afstand<strong>en</strong> geeft. Terugker<strong>en</strong>d<br />

van dm−1,n−1 kan immers bepaald word<strong>en</strong> welke van de drie toegelat<strong>en</strong> bewerking<strong>en</strong><br />

aanleiding gegev<strong>en</strong> heeft tot de waarde van dm−1,n−1.<br />

Voorbeeld 2.1.4. Bij<strong>voor</strong>beeld, <strong>voor</strong> de bov<strong>en</strong>staande tabel is d5,5 = 3 omdat d4,4 = 2 <strong>en</strong> S[5] =<br />

T[5], terwijl d5,4 = 3 <strong>en</strong> d4,5 = 3. Hieruit kunn<strong>en</strong> we besluit<strong>en</strong> dat de laatste bewerking in de<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


2.2. Beste b<strong>en</strong>ader<strong>en</strong>de match 23<br />

goedkoopste omzetting de wijziging van S[5] = p in T[5] = a is. Vervolg<strong>en</strong>s bekijk<strong>en</strong> we hoe<br />

de waarde van d4,4 = 2 bepaald werd, nl. uit d3,3 = 2 (ge<strong>en</strong> bewerking). De waarde d3,3 = 2<br />

komt van d3,2 = 1 met e<strong>en</strong> tuss<strong>en</strong>voeging van e op positie 3. De waarde d3,2 = 2 keert terug<br />

tot d2,1 = 1 (ge<strong>en</strong> bewerking), die op zijn beurt afkomstig is van d1,0 = 1 (ge<strong>en</strong> bewerking). De<br />

waarde d1,0 = 1 is bepaald uit d0,0 = 0 (verwijder r op positie 1), die op zijn beurt teruggaat<br />

op d−1,−1 = 0 (ge<strong>en</strong> bewerking). Dit levert ons de reeks bewerking<strong>en</strong> die presto omzett<strong>en</strong><br />

in peseta: verwijder r op positie 1 (dit levert pesto), voeg e tuss<strong>en</strong> op positie 3 (dit levert<br />

peseto), wijzig o in a op positie 5 (dit levert peseta).<br />

2.2 Beste b<strong>en</strong>ader<strong>en</strong>de match<br />

Bij het zoek<strong>en</strong> van e<strong>en</strong> patroon in e<strong>en</strong> tekst zijn we dikwijls geïnteresseerd in het vind<strong>en</strong> van e<strong>en</strong><br />

b<strong>en</strong>ader<strong>en</strong>de match, eerder dan e<strong>en</strong> exacte match. In e<strong>en</strong> algem<strong>en</strong>e formulering van het probleem<br />

is e<strong>en</strong> patroon P <strong>en</strong> e<strong>en</strong> tekst T gegev<strong>en</strong> <strong>en</strong> prober<strong>en</strong> we e<strong>en</strong> gedeelte van de tekst te vind<strong>en</strong> dat<br />

zo sterk mogelijk gelijkt op P. De oplossing hangt dan af van de maat van gelijk<strong>en</strong>is die we<br />

gebruik<strong>en</strong>. Wanneer we de editeerafstand gebruik<strong>en</strong>, dan wordt het probleem het volg<strong>en</strong>de:<br />

Gegev<strong>en</strong> e<strong>en</strong> patroon P <strong>en</strong> e<strong>en</strong> tekst T , zoek e<strong>en</strong> deelwoord W = T[i.. j] zodanig dat<br />

de editeerafstand d(P,W) zo klein mogelijk is.<br />

Het woord W wordt beschouwd als e<strong>en</strong> b<strong>en</strong>ader<strong>en</strong>de match <strong>en</strong> d(W,P) is e<strong>en</strong> maat van hoe goed<br />

de match is.<br />

Bij<strong>voor</strong>beeld, de beste match <strong>voor</strong> het patroon retrieve in de tekst<br />

retreive, retreeve, retreev<br />

is het deelwoord retreeve start<strong>en</strong>d op positie 10, omdat het afstand 1 heeft (nl. i vervang<strong>en</strong><br />

door e).<br />

E<strong>en</strong> e<strong>en</strong>voudige manier om de beste b<strong>en</strong>ader<strong>en</strong>de match te vind<strong>en</strong> bestaat erin om de editeerafstand<br />

te berek<strong>en</strong><strong>en</strong> tuss<strong>en</strong> het patroon P <strong>en</strong> alle mogelijke deelwoord<strong>en</strong> van T , <strong>en</strong> de kleinste<br />

afstand daarin te bepal<strong>en</strong>. Zij m = |P| <strong>en</strong> n = |T|. Er zijn n 2 deelwoord<strong>en</strong> van T <strong>en</strong> het vergelijk<strong>en</strong><br />

van elk daarvan met P kost O(mn), zodat de totale tijdscomplexiteit O(mn 3 ) wordt.<br />

Dit e<strong>en</strong>voudige algoritme is echter in strijd met e<strong>en</strong> van de basisregels van dynamisch programmer<strong>en</strong>,<br />

nl. dat informatie die reeds berek<strong>en</strong>d werd niet opnieuw mag berek<strong>en</strong>d word<strong>en</strong>. Immers,<br />

beschouw het berek<strong>en</strong><strong>en</strong> van de editeerafstand op P <strong>en</strong> T . Het algoritme berek<strong>en</strong>t waard<strong>en</strong><br />

d(P[0..i],T[0.. j]), <strong>voor</strong> alle 0 ≤ i < m <strong>en</strong> 0 ≤ j < n. Het geeft dus de editeerafstand<strong>en</strong> van P tot<br />

alle prefix<strong>en</strong> van T . Dit komt reeds zeer dicht bij wat we nodig hebb<strong>en</strong>, met dit verschil dat we<br />

ook de editeerafstand<strong>en</strong> tot suffix<strong>en</strong> van T[0.. j] nodig hebb<strong>en</strong>, <strong>voor</strong> alle j.<br />

We definiër<strong>en</strong><br />

adi, j = min{d(P[0..i],T[ℓ, j]) | 0 ≤ ℓ ≤ j+ 1},<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


24 Hoofdstuk 2. <strong>Algoritm<strong>en</strong></strong> <strong>voor</strong> b<strong>en</strong>ader<strong>en</strong>d string-matching<br />

m.a.w. adi, j is de kleinste editeerafstand tuss<strong>en</strong> P[0,i] <strong>en</strong> e<strong>en</strong> deelwoord van T dat eindigt in j<br />

(mogelijks de lege string, als ℓ = j + 1). De waarde die we zoek<strong>en</strong>, is dan het minimum<br />

van adm−1,0,adm−1,1,...,adm−1,n−1. De nieuwe tabel adi, j ziet er ingewikkelder uit dan di, j,<br />

maar kan berek<strong>en</strong>d word<strong>en</strong> door gebruik te mak<strong>en</strong> van dezelfde formule als <strong>voor</strong> de berek<strong>en</strong>ing<br />

van di, j:<br />

⎧ <br />

⎪⎨<br />

0 als P[i] = T[ j]<br />

adi−1, j−1 +<br />

1 anders<br />

adi, j = min<br />

⎪⎩<br />

adi, j−1 + 1<br />

adi−1, j + 1<br />

De juistheid van deze formule kan op dezelfde manier geargum<strong>en</strong>teerd word<strong>en</strong> als bij de formule<br />

<strong>voor</strong> di, j. Wanneer P[i] = T[ j], dan kunn<strong>en</strong> we de match gevond<strong>en</strong> door adi−1, j−1 uitbreid<strong>en</strong>.<br />

Wanneer P[i] = T[ j], dan zijn er drie mogelijkhed<strong>en</strong> <strong>voor</strong> hoe de laatste letter van P[0..i] <strong>en</strong><br />

T[ℓ.. j] match<strong>en</strong>: vervang<strong>en</strong> van P[i] door T[ j], verwijder<strong>en</strong> van P[i], of toevoeg<strong>en</strong> van T[ j] aan<br />

het einde van P.<br />

De berek<strong>en</strong>ing van de tabel adi, j verschilt van de berek<strong>en</strong>ing van di, j in de initialisatie. In de<br />

definitie van adi, j wordt als extreem geval toegelat<strong>en</strong> dat P[0..i] tot de lege string herleid wordt.<br />

M.a.w. ad−1, j = 0 <strong>voor</strong> alle j (omdat de lege string e<strong>en</strong> deelstring is van om het ev<strong>en</strong> welke string<br />

op om het ev<strong>en</strong> welke positie j).<br />

Algoritme 2.2 geeft de pseudocode <strong>voor</strong> deze berek<strong>en</strong>ing. De complexiteit van dit algoritme<br />

is O(mn).<br />

Bepal<strong>en</strong> we bij<strong>voor</strong>beelde de beste b<strong>en</strong>ader<strong>en</strong>de match <strong>voor</strong> het patroon P = gogle in de tekst<br />

T = internet,google,search. We bekom<strong>en</strong> de volg<strong>en</strong>de tabel:<br />

−1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1<br />

i n t e r n e t , g o o g l e , s e a r c h<br />

−1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0<br />

0 g 1 1 1 1 1 1 1 1 1 1 0 1 1 0 1 1 1 1 1 1 1 1 1<br />

1 o 2 2 2 2 2 2 2 2 2 2 1 0 1 1 1 2 2 2 2 2 2 2 2<br />

2 g 3 3 3 3 3 3 3 3 3 3 2 1 1 1 2 2 3 3 3 3 3 3 3<br />

3 l 4 4 4 4 4 4 4 4 4 4 3 2 2 2 1 2 3 4 4 4 4 4 4<br />

4 e 5 5 5 5 4 5 5 4 5 5 4 3 3 3 2 1 2 3 4 5 5 5 5<br />

De kleinste waarde in de laatste rij is 1, <strong>en</strong> dus hebb<strong>en</strong> we e<strong>en</strong> match <strong>voor</strong> gogle van die afstand.<br />

Door de 1 achterwaarts te volg<strong>en</strong>, vind<strong>en</strong> we dat de match op posities 9 t.e.m. 14 <strong>voor</strong>komt.<br />

Noem<strong>en</strong> we e<strong>en</strong> match W e<strong>en</strong> k-b<strong>en</strong>ader<strong>en</strong>de match <strong>voor</strong> P als d(P,W) ≤ k. Dan kunn<strong>en</strong> we<br />

de vraag stell<strong>en</strong> of er, <strong>voor</strong> e<strong>en</strong> gegev<strong>en</strong> patroon P <strong>en</strong> e<strong>en</strong> gegev<strong>en</strong> tekst T , e<strong>en</strong> k-b<strong>en</strong>ader<strong>en</strong>de<br />

match bestaat. Algoritme 2.2 lost dit probleem op in tijd O(mn).<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be


2.2. Beste b<strong>en</strong>ader<strong>en</strong>de match 25<br />

Algoritme 2.2 Bepal<strong>en</strong> van de beste b<strong>en</strong>ader<strong>en</strong>de match (in editeerafstand) tuss<strong>en</strong> e<strong>en</strong> patroon P<br />

<strong>en</strong> e<strong>en</strong> tekst T<br />

Input: patroon P van l<strong>en</strong>gte m, tekst T van l<strong>en</strong>gte n<br />

Output: de kleinst mogelijke editeerafstand tuss<strong>en</strong> P <strong>en</strong> e<strong>en</strong> deelwoord van T<br />

1: {Initialiser<strong>en</strong> van kolom −1}<br />

2: for i from −1 to m − 1 do<br />

3: Stel ad[i,−1] ← i+1<br />

4: {Initialiser<strong>en</strong> van rij −1}<br />

5: for j from 0 to n − 1 do<br />

6: Stel ad[−1, j] ← 0<br />

7: {Berek<strong>en</strong>ing van de eig<strong>en</strong>lijke tabel}<br />

8: for i from 0 to m − 1 do<br />

9: for j from 0 to n − 1 do<br />

10: if P[i] = T[ j] th<strong>en</strong><br />

11: Stel x ← ad[i − 1, j − 1]<br />

12: else<br />

13: Stel x ← 1+ad[i − 1, j − 1]<br />

14: Stel ad[i, j] ← min(x,1+ad[i, j − 1],1+ad[i − 1, j])<br />

15: {Bepal<strong>en</strong> van de kleinste editeerafstand}<br />

16: Stel a ← m<br />

17: for j from 0 to n − 1 do<br />

18: if ad[m − 1, j] < a th<strong>en</strong><br />

19: Stel a ← ad[m − 1, j]<br />

20: return a<br />

<strong>Algoritm<strong>en</strong></strong> <strong>en</strong> Datastructur<strong>en</strong> <strong>III</strong> Veerle.Fack@UG<strong>en</strong>t.be

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!