Algoritm Knuth-Morris-Pratt

De la Wikipedia, enciclopedia liberă.
Salt la navigare Salt la căutare

Algoritmul Knuth-Morris-Pratt (adesea prescurtat ca algoritm KMP ) este un algoritm de potrivire a modelelor pe șiruri , care permite găsirea aparițiilor unui șir (model) într-un text . Particularitatea sa constă în tratarea prealabilă a șirului de căutat, care conține indicația suficientă pentru a determina poziția din care să continue căutarea în caz de necorespondență. Acest lucru permite algoritmului să nu reexamineze caracterele care au fost verificate anterior și, prin urmare, să limiteze numărul de comparații necesare.

Algoritmul a fost inventat de Knuth și Pratt și independent de JH Morris în 1975 .

Principiul de funcționare

Abordare banală

Pentru a înțelege mai bine logica algoritmului Knuth-Morris-Pratt, este bine să înțelegem abordarea banală a problemei.

Șirul B poate fi găsit în textul A cu următorul algoritm:

  1. Sigur ;
  2. Atâta timp cât există poziții de analizat
    • Comparați șirul B și textul A literă cu literă începând de la poziție ;
    • Dacă șirul a fost găsit, atunci terminați tratamentul și reveniți ca poziție inițială a apariției;
    • În caz contrar, remediați ;
  3. Finalizați căutarea, nu s-au găsit apariții.

Această procedură poate fi îmbunătățită prin întreruperea comparației la al treilea pas, imediat ce este găsit un caracter diferit, fără a verifica întregul șir.

Această soluție are un dezavantaj: după o comparație nereușită, următoarea comparație va începe de la poziție , fără a lua în considerare acele comparații care au fost făcute în pasul anterior, adică poziția . Algoritmul Knuth-Morris-Pratt examinează mai întâi șirul B deducând informații care vă permit să evitați tratarea fiecărui caracter de mai multe ori.

Etape

  1. Prima fază a algoritmului construiește un tabel, care indică o „schimbare de fază” pentru fiecare poziție, adică următoarea poziție în care este posibil să se găsească o potențială apariție a șirului B.
  2. A doua fază efectuează căutarea efectivă, comparând caracterele șirului care trebuie căutat cu cele ale textului. În caz de diferență, utilizați tabelul pentru a cunoaște „defazarea” care trebuie luată în considerare pentru a continua căutarea fără a vă întoarce.

Exemplu

Pentru a prezenta principiul funcționării algoritmului, să luăm în considerare un exemplu particular: șirul este ABCDABD în timp ce textul este ABC ABCDAB ABCDABCDABDE .

Notare : Pentru a reprezenta șiruri de caractere, în această intrare vom folosi tabele în care indexurile încep de la zero. Prin urmare, C al șirului va fi exprimat ca . desemnează poziția, în text , la care șirul este verificat, e poziția personajului în curs de verificare .

 1 2
    01234567890123456789012
m: v
S: ABC ABCDAB ABCDABCDABDE
P: ABCDABD
i: ^
    0123456

Algoritmul începe prin testarea potrivirii de caractere, unul după altul. Deci, la a patra treaptă, Și . este un spațiu și , potrivirea nu este posibilă.

 1 2
    01234567890123456789012
m: v
S: ABC ABCDAB ABCDABCDABDE
P: ABCDABD
i: ^
    0123456

Decât să o iau de la început , Algoritmul ia în considerare faptul că nici un prezent în era între pozițiile 0 și 3, cu excepția poziției 0. În consecință, deoarece toate caracterele precedente au fost testate, algoritmul știe că nu există nicio posibilitate de a găsi începutul unei potriviri dacă se verifică din nou. Din acest motiv, algoritmul avansează la următorul caracter în care ar putea exista o posibilă apariție, prin plasare Și (este important să rețineți că mai întâi devine cu , la fel de , atunci, deoarece nu există corespondență, devine cu , la fel de ; consultați algoritmul de mai jos pentru clarificări pe tabel ).

 1 2
    01234567890123456789012
m: v
S: ABC ABCDAB ABCDABCDABDE
P: ABCDABD
i: ^
        0123456

O corespondență aproape completă se obține când cu m = 4 și cu , verificarea eșuează.

 1 2
    01234567890123456789012
m: v
S: ABC ABCDAB ABCDABCDABDE
P: ABCDABD
i: ^
        0123456

Cu toate acestea, chiar înainte de sfârșitul acestui meci parțial, algoritmul a trecut la modelul AB , care ar putea fi începutul unui alt meci. Prin urmare, aceste informații trebuie luate în considerare. Deoarece algoritmul știe deja că primele două caractere se potrivesc celor două caractere care preced poziția curentă, nu este nevoie să le verificăm din nou. Apoi, algoritmul reia tratamentul la caracterul curent, cu Și .

 1 2
    01234567890123456789012
m: v
S: ABC ABCDAB ABCDABCDABDE
P: ABCDABD
i: ^
            0123456

Această verificare eșuează imediat ( C nu se potrivește cu spațiul din ). Deoarece șirul nu conține spații (ca în primul pas), algoritmul continuă căutarea de la și reinitializarea (ca mai sus, de fapt mai întâi devine cu , la fel de , atunci, deoarece nu există corespondență, devine cu , la fel de ).

 1 2
    01234567890123456789012
m: v
S: ABC ABCDAB ABCDABCDABDE
P: ABCDABD
i: ^
               0123456

Din nou, algoritmul găsește o potrivire parțială ABCDAB , dar următorul caracter C nu se potrivește cu caracterul final D al șirului.

 1 2
    01234567890123456789012
m: v
S: ABC ABCDAB ABCDABCDABDE
P: ABCDABD
i: ^
               0123456

Folosind același raționament ca înainte, algoritmul reia cu , pentru a reporni comparația începând de la cele două caractere AB , fixând ca nouă locație curentă.

 1 2
    01234567890123456789012
m: v
S: ABC ABCDAB ABCDABCDABDE
P: ABCDABD
i: ^
                   0123456

De data aceasta, potrivirea dintre șir și text este completă, astfel încât algoritmul returnează poziția 15 (adică ) ca punct de plecare.

 1 2
    01234567890123456789012
m: v
S: ABC ABCDAB ABCDABCDABDE
P: ABCDABD
i: ^
                   0123456

Algoritmul de căutare

Exemplul anterior ilustrează intuitiv principiul de funcționare al algoritmului. Adică, presupune prezența unui tabel de „potriviri parțiale” (vezi articolul de mai jos), care indică începutul probabil al următoarei apariții, în cazul în care verificarea apariției curente eșuează. Deocamdată, acest tabel, cu care denotăm , poate fi considerat ca o cutie neagră care are următoarea proprietate: dacă avem o potrivire parțială până la , dar asta eșuează atunci când comparăm Și , apoi următoarea apariție parțială va începe de la poziție . În special, există și este plasat la . Având acest tabel, algoritmul este relativ simplu:

  1. Sigur . să presupunem că au o lungime de personaje, ed din personaje;
  2. De sine , apoi terminați comparația, nu a fost găsită nicio potrivire. În caz contrar, comparați Și ;
    • Dacă sunt la fel, atunci remediați . De sine , atunci meciul este complet. Încheiați comparația și reveniți ca poziție inițială a corespondenței;
    • Dacă sunt diferite, remediați , si daca , sigur ;
  3. Reluați de la pasul 2.

Această descriere pune în practică algoritmul utilizat în exemplul anterior. Ori de câte ori apare o eroare de verificare, tabelul este consultat pentru a găsi începutul următoarei apariții potențiale și contoare sunt actualizate în consecință. Ca urmare, verificarea caracterului nu se face niciodată înapoi. În special, fiecare caracter este verificat o singură dată (cu excepția cazului în care poate fi eliminat de mai multe ori în urma unei nepotriviri, a se vedea mai jos eficiența algoritmului).

Exemplu de cod pentru algoritmul de căutare

Următorul cod C este o implementare a acestui algoritm.

 int kmp_research ( 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;
        } altceva {
            m + = i - T [i];
            if (i> 0) i = T [i];
        }
    }
    
    if (P [i] == '\ 0') {
        retur m;
    } altceva {
        returnează m + i;
    }
}

Eficiența algoritmului de căutare

Presupunând existența unui tabel , faza de „cercetare” a algoritmului Knuth-Morris-Pratt este de complexitate O , unde este desemnează lungimea . Dacă excludem tratamentul fix suplimentar, indus de începutul și sfârșitul funcției, toate tratamentele se efectuează în ciclul principal. Pentru a calcula o limită a numărului de iterații, este necesară o primă observație cu privire la natura . Prin definiție, este construit astfel încât, dacă un meci parțial începe de la eșuează la comparare Și , următorul potențial meci nu începe înainte . În special, următorul meci potențial trebuie localizat cu o poziție mai târziu , astfel încât .

Pornind de la această ipoteză, se arată că ciclul este realizat la maxim ori. La fiecare iterație, una dintre cele două ramuri ale instrucțiunii if este executată.

  • prima ramură crește invariabil și nu modifică , astfel încât indexul a caracterelor comparate în șir este mărită.
  • a doua ramură crește din . Fiind întotdeauna pozitiv, așa cum am văzut anterior, deducem că indicele de la începutul posibilului meci este mărită.

Ciclul se încheie dacă , ceea ce înseamnă, având în vedere convenția C că caracterul NUL indică sfârșitul unui șir, că . În consecință, fiecare ramură a instrucțiunii if poate fi executată cel mult ori, deoarece cele două ramuri cresc respectiv sau sau , cu ; astfel încât dacă , asa de , și fiind creșterea la fiecare ciclu a cel puțin o unitate, este neapărat adevărat în trecut.

Prin urmare, ciclul se desfășoară la maximum ori, deci complexitatea de calcul este .

Tabelul „corespondențelor parțiale”

Scopul acestui tabel este de a permite algoritmului să nu verifice fiecare caracter al textului de mai multe ori. Observația cheie pentru a stabili natura liniară a căutării, care permite acest algoritm să funcționeze, este că după verificarea unei bucăți de text care conține o "porțiune de pornire" a șirului, este posibil să se determine în ce poziții pot apărea următoarele posibile apariții începe.și din ele continuă comparația începând de la poziția actuală a textului. Cu alte cuvinte, motivele (sub-porțiunile șirului) sunt „preidentificate” în șir și se creează o listă care indică toate pozițiile posibile din care să continue, sărind cel mai mare număr de caractere inutile, fără a sacrifica orice potrivire potențială.

Pentru fiecare poziție din șir, este necesar să se determine lungimea maximă a motivului de pornire, care se termină în poziția curentă, dar care nu permite o potrivire completă (și, prin urmare, probabil va eșua). Prin urmare, indică exact lungimea maximă a motivului de început care se termină cu . Prin convenție, șirul nul are lungimea zero. Deoarece verificarea inițială a șirului este un caz particular (deoarece nu există nicio posibilitate de backtracking ), apare , după cum sa discutat mai sus.

Descrierea pseudocodului

Principiul este cel al cercetării în general: o mare parte a muncii a fost deja realizată pentru a ajunge la poziția actuală și, prin urmare, a rămas puțin. Folosim partea deja populată a tabelului pentru a găsi potențiale șiruri de caractere, similar algoritmului de căutare. Singura complicație minoră este că logica care este corectă mai târziu, din păcate, returnează șiruri incorecte la început. Această problemă necesită un anumit cod de inițializare.

 algoritm kmp_table :
    intrare :
        o serie de caractere, W (cuvântul care trebuie analizat)
        o serie de numere întregi, T (tabelul care trebuie completat)
    ieșire :
        nimic (dar în timpul operației populăm tabelul)

    definiți variabilele :
        un număr întreg, pos ← 2 (poziția curentă a calculului lui T)
        un întreg, cnd ← 0 (indexul începând de la zero în W al următorului caracter al șirului candidat)

    (primele valori sunt fixe, dar diferite de ceea ce ar putea sugera algoritmul)
    Fie T [0] ← -1, T [1] ← 0

    în timp ce poziția este mai mică decât lungimea lui W, faceți:
        (primul caz: șirul continuu)
        în cazul în care W [pos - 1] = W [CND], lăsați T [pos] ← + 1 cnd, pos pos ← + 1, ← cnd + 1 CND

        (al doilea caz: nu, dar nu ne putem întoarce)
        altfel , dacă cnd> 0,  cnd ← T [cnd]

        (al treilea caz: rămânem fără candidați. Notă cnd = 0)
        altfel , lasă T [pos] ← 0, pos ← pos + 1

Eficiența construcției mesei

Complexitatea algoritmului tabelului este O(n) , unde n este lungimea lui W În afară de inițializare, toate lucrările se realizează în bucla while , arătând doar că bucla execută O(n) ori, ceea ce ar trebui făcut prin examinarea simultană a valorilor pos și pos - cnd .

  • În prima ramură, pos - cnd rămâne constant, deoarece pos și cnd sunt incrementate împreună, dar, desigur, pos crește continuu.
  • În cel de-al doilea caz, cnd este înlocuit cu T[cnd] , pe care l-am văzut a fi strict mai mic decât cnd , astfel încât pos - cnd este crescut.
  • În al treilea caz, pos este crescut, în timp ce cnd rămâne stabil și, astfel, pos și pos - cnd crește.

Deoarece pos ≥ pos - cnd , aceasta înseamnă că la fiecare ciclu crește ambele pos și o cantitate mai mică decât pos; de aceea, deoarece algoritmul se termină când se atinge pos = n , acesta trebuie să se termine după cel mult 2n iterații, deoarece pos - cnd începe de la 1 . Deci complexitatea algoritmului tabelului este O(n) .

Eficiența algoritmului KMP

Deoarece cele două părți ale algoritmului au, respectiv, complexități O(k) și O(n) , complexitatea totală este O(n + k) .

Bibliografie

Elemente conexe

linkuri externe

Informatică Portal IT : accesați intrările Wikipedia care se ocupă cu IT