Algoritmo di Knuth-Morris-PrattL'algoritmo di Knuth-Morris-Pratt (spesso abbreviato come algoritmo KMP) è un algoritmo di pattern matching su stringhe, che permette di trovare le occorrenze di una stringa (pattern) in un testo . La sua peculiarità risiede nel pretrattamento della stringa da cercare, la quale contiene l'indicazione sufficiente a determinare la posizione da cui continuare la ricerca in caso di non-corrispondenza. Questo permette all'algoritmo di non riesaminare i caratteri che erano stati precedentemente verificati, e dunque di limitare il numero di confronti necessari. L'algoritmo è stato inventato da Knuth e Pratt, e indipendentemente da J. H. Morris nel 1975. Principio di funzionamentoApproccio banaleAl fine di comprendere meglio la logica dell'algoritmo di Knuth-Morris-Pratt, è bene comprendere l'approccio banale al problema. La sottostringa B può essere trovata nel testo A con l'algoritmo seguente:
Questa procedura può essere migliorata interrompendo la comparazione al terzo passo, appena trovato un carattere che differisce, senza verificare l'intera stringa. Questa soluzione ha un inconveniente: dopo una comparazione infruttuosa, la comparazione seguente inizierà alla posizione , senza tenere in considerazione quelle comparazioni che sono state fatte al passo precedente, cioè alla posizione . L'algoritmo di Knuth-Morris-Pratt prima esamina la stringa B deducendo delle informazioni che permettono di evitare di trattare ogni singolo carattere più di una volta. Fasi
EsempioPer presentare il principio di funzionamento dell'algoritmo, consideriamo un esempio particolare: la stringa è ABCDABD mentre il testo è ABC ABCDAB ABCDABCDABDE. Notazione: Per rappresentare le stringhe di caratteri, in questa voce si utilizzeranno delle tabelle nelle quali gli indici iniziano da zero. Dunque, il C della stringa sarà espresso come . designa la posizione, nel testo , alla quale la stringa è in corso di verifica, e la posizione del carattere attualmente verificato in . 1 2 01234567890123456789012 m : v S : ABC ABCDAB ABCDABCDABDE P : ABCDABD i : ^ 0123456 L'algoritmo inizia testando la corrispondenza dei caratteri, uno dopo l'altro. Quindi, al quarto passo, e . è uno spazio e , la corrispondenza non è possibile. 1 2 01234567890123456789012 m : v S : ABC ABCDAB ABCDABCDABDE P : ABCDABD i : ^ 0123456 Piuttosto che ricominciare da , l'algoritmo tiene conto che nessuna A era presente in tra le posizioni 0 e 3, ad eccezione della posizione 0. Di conseguenza, poiché sono stati testati tutti i caratteri precedenti, l'algoritmo sa che non c'è possibilità di trovare l'inizio di una corrispondenza se verifica nuovamente. Per questa ragione, l'algoritmo avanza fino al carattere successivo in cui potrebbe esserci una eventuale occorrenza, ponendo e (è importante notare che prima diventa con , in quanto , poi non essendoci corrispondenza diventa con , in quanto ; si veda l'algoritmo più avanti per chiarimenti sulla tabella ). 1 2 01234567890123456789012 m : v S : ABC ABCDAB ABCDABCDABDE P : ABCDABD i : ^ 0123456 Una corrispondenza quasi completa è ottenuta quando con m=4 e con , la verifica fallisce. 1 2 01234567890123456789012 m : v S : ABC ABCDAB ABCDABCDABDE P : ABCDABD i : ^ 0123456 Tuttavia, appena prima della fine di questa corrispondenza parziale, l'algoritmo è passato sul motivo AB, che potrebbe essere l'inizio di un'altra corrispondenza. Questa informazione dev'essere dunque tenuta in conto. Poiché l'algoritmo sa già che questi primi due caratteri corrispondono con i due caratteri che precedono la posizione corrente, non è necessario di verificarli nuovamente. Quindi, l'algoritmo riprende il trattamento al carattere corrente, con e . 1 2 01234567890123456789012 m : v S : ABC ABCDAB ABCDABCDABDE P : ABCDABD i : ^ 0123456 Questa verifica fallisce immediatamente (C non corrisponde allo spazio in ). Poiché la stringa non contiene alcuno spazio (come nel primo passo), l'algoritmo prosegue la ricerca da e reinizializzando (come sopra, in realtà prima diventa con , in quanto , poi non essendoci corrispondenza diventa con , in quanto ). 1 2 01234567890123456789012 m : v S : ABC ABCDAB ABCDABCDABDE P : ABCDABD i : ^ 0123456 Nuovamente, l'algoritmo trova una corrispondenza parziale ABCDAB, ma il carattere seguente C non corrisponde al carattere finale D della stringa. 1 2 01234567890123456789012 m : v S : ABC ABCDAB ABCDABCDABDE P : ABCDABD i : ^ 0123456 Usando lo stesso ragionamento di prima, l'algoritmo riprende con , per ricominciare il confronto a partire dai due caratteri AB, fissando come nuova posizione corrente. 1 2 01234567890123456789012 m : v S : ABC ABCDAB ABCDABCDABDE P : ABCDABD i : ^ 0123456 Questa volta, la corrispondenza tra stringa e testo è completa, quindi l'algoritmo restituisce la posizione 15 (cioè ) come punto d'inizio. 1 2 01234567890123456789012 m : v S : ABC ABCDAB ABCDABCDABDE P : ABCDABD i : ^ 0123456 L'algoritmo di ricercaL'esempio precedente illustra in modo intuitivo il principio di funzionamento dell'algoritmo. Suppone, cioè, la presenza di una tabella di « corrispondenze parziali » (vedi seguito dell'articolo), che indica il probabile inizio della prossima occorrenza, nel caso in cui la verifica dell'occorrenza attuale fallisca. Per ora, questa tabella, che indichiamo con , può essere considerata come una black box che ha la proprietà seguente: se abbiamo a disposizione una corrispondenza parziale fino a , ma che fallisce quando compariamo e , allora la prossima occorrenza parziale inizierà alla posizione . In particolare, esiste ed è posta a . Avendo questa tabella, l'algoritmo è relativamente semplice:
Questa descrizione mette in pratica l'algoritmo usato nell'esempio precedente. Ogni volta che avviene un errore nella verifica, viene consultata la tabella per trovare l'inizio della prossima occorrenza potenziale, ed i contatori sono aggiornati di conseguenza. Di conseguenza, la verifica dei caratteri non è mai fatta all'indietro. In particolare, ogni carattere non è verificato che una volta soltanto (fatto salvo che possa essere scartato più volte in seguito ad una non-corrispondenza, si veda più in basso per l'efficienza dell'algoritmo). Esempio di codice per l'algoritmo di ricercaIl codice C che segue è un'implementazione di questo algoritmo. int kmp_ricerca(char *P, char *S) { extern int T[]; int m = 0; int i = 0; while (S[m + i] != '\0' && P[i] != '\0') { if (S[m + i] == P[i]) { ++i; } else { m += i - T[i]; if (i > 0) i = T[i]; } } if (P[i] == '\0') { return m; } else { return m + i; } } Efficienza dell'algoritmo di ricercaSupponendo l'esistenza di una tabella , la fase « ricerca » dell'algoritmo di Knuth-Morris-Pratt è di complessità O, dove designa la lunghezza di . Se si esclude il trattamento supplementare fissato, indotto dall'inizio e dalla fine della funzione, tutti i trattamenti sono effettuati nel ciclo principale. Per calcolare un limite sul numero d'iterazioni, è necessaria una prima osservazione riguardo della natura di . Per definizione, è costruita in maniera che se una corrispondenza parziale che inizia a fallisce durante il confronto tra e , la prossima corrispondenza potenziale non inizia prima di . In particolare, la potenziale corrispondenza successiva deve trovarsi una posizione dopo , in modo che . Partendo da questa assunzione, si dimostra che il ciclo viene eseguito al massimo volte. Ad ogni iterazione, viene eseguito uno dei due rami dell'istruzione if.
Il ciclo termina se , il che vuol dire, tenendo presente la convenzione del C secondo cui il carattere NUL indica la fine di una stringa, che . Conseguentemente, ogni ramo dell'istruzione if può essere eseguito al più volte, dato che i due rami aumentano rispettivamente o o , con ; cosicché se , allora , ed essendo l'aumento ad ogni ciclo almeno di una unità, è necessariamente vero per il passato. Il ciclo dunque viene eseguito al massimo volte, quindi la complessità computazionale è . La tavola delle « corrispondenze parziali »Lo scopo di questa tabella è quello di permettere all'algoritmo di non controllare ogni carattere del testo più di una volta. L'osservazione chiave per stabilire la natura lineare della ricerca, che consente a questo algoritmo di funzionare, è che dopo aver verificato una porzione di testo contenente una « porzione iniziale » della stringa, è possibile determinare in quale posizioni possono incominciare le successive possibili occorrenze e da esse continuare il confronto a partire dalla posizione corrente del testo. In altri termini, i motifs (le sotto-porzioni della stringa) vengono « pre-identificati » nella stringa e viene creata una lista che indica tutte le posizioni possibili da cui continuare saltando il maggior numero di caratteri inutili, senza tuttavia sacrificare alcuna corrispondenza potenziale. Per ciascuna posizione nella stringa, è necessario determinare la lunghezza massima del motif iniziale, che termina nella posizione corrente, ma che non consente una corrispondenza completa (e che quindi probabilmente fallirà). Quindi, indica esattamente la massima lunghezza del motif iniziale che termina in . Per convenzione, la stringa nulla ha lunghezza zero. Essendo la verifica iniziale della stringa un caso particolare (non essendoci la possibilità del backtracking), si pone , come discusso in precedenza. Descrizione dello pseudocodiceIl principio è quello della ricerca in generale: buona parte del lavoro è già stato fatto nel raggiungere la posizione corrente, e quindi ce ne resta poco. Utilizziamo la parte già popolata della tabella per trovare potenziali sottostringhe, in modo analogo all'algoritmo di ricerca. La sola piccola complicazione è che la logica che in seguito è corretta, purtroppo restituisce sottostringhe non corrette all'inizio. Questo problema richiede del codice di inizializzazione. algoritmo kmp_table: input: un array di caratteri, W (la parola da analizzare) un array di interi, T (la tabella da riempire) output: niente (ma durante l'operazione popoliamo la tabella) definire le variabili: un intero, pos ← 2 (la posizione corrente della computazione di T) un intero, cnd ← 0 (l'indice che parte da zero in W del prossimo carattere della sottostringa candidata) (i primi pochi valori sono fissi ma diversi da quelli che l'algoritmo potrebbe suggerire) let T[0] ← -1, T[1] ← 0 while pos è minore della lunghezza di W, do: (primo caso: la sottostringa continua) if W[pos - 1] = W[cnd], let T[pos] ← cnd + 1, pos ← pos + 1, cnd ← cnd + 1 (secondo caso: non lo fa ma non possiamo tornare indietro) else, if cnd > 0, let cnd ← T[cnd] (terzo caso: abbiamo finito i candidati. Nota cnd = 0) else, let T[pos] ← 0, pos ← pos + 1 Efficienza della costruzione della tabellaLa complessità dell'algoritmo della tabella è
Dal momento che Efficienza dell'algoritmo KMPDal momento che le due parti dell'algoritmo hanno, rispettivamente, complessità di Bibliografia
Voci correlateCollegamenti esterni
|
Portal di Ensiklopedia Dunia