De ce contează?
Cauți un cuvânt anume într-o pagină de carte. Nu citești tot — îți plimbi privirea de la stânga la dreapta și, la fiecare loc, verifici dacă literele de acolo formează cuvântul tău. Dacă prima literă nu se potrivește, treci imediat mai departe; dacă se potrivește, verifici și a doua, a treia... Exact așa cauți un șablon (sub-șir) într-un text.
Intuiția
Ai un text (de exemplu abracadabra) și un șablon (de exemplu abra),
și vrei să afli unde apare șablonul în text. Ideea de bază e simplă: așezi
șablonul peste text începând de la poziția 0, verifici caracter cu caracter,
apoi îl glisezi o poziție la dreapta și încerci din nou. Faci asta la fiecare
poziție posibilă de start. Acolo unde toate caracterele șablonului coincid cu
textul, ai găsit o apariție.
Vezi cum funcționează
Urmărește cum șablonul abra alunecă peste textul abracadabra: la fiecare
poziție se aliniază și compară litere; la prima nepotrivire sare mai departe, iar
când toate coincid marchează o apariție.
Folosește ← și → ca să pășești comparație cu comparație, sau Redă pentru animație. Observă cum, la pozițiile care încep cu litera greșită, șablonul sare imediat — fără să mai compare restul.
Cum cauți din instinct? Pui șablonul la fiecare poziție i din text și compari
literele una câte una. Două bucle: una mută poziția de start i prin text, alta
parcurge șablonul cu j și verifică text[i+j] == sablon[j]. Cost: în cel mai
rău caz O(n*m), unde n e lungimea textului și m a șablonului.
La texte foarte mari cu multe potriviri parțiale lungi (de pildă text plin de
aaaa... și șablon aaab), de la fiecare poziție compari aproape tot șablonul
înainte să eșuezi — multă muncă repetată, refăcută la fiecare pas. Există un
algoritm (KMP) care folosește ce a comparat deja ca să nu repete; îl vei întâlni
într-o lecție viitoare.
Pentru clasa a 8-a, căutarea naivă e suficientă și foarte clară: mergi poziție cu poziție și, la fiecare, ieși din verificare la prima nepotrivire. Dacă un caracter nu coincide, șablonul sigur nu apare începând de acolo, deci nu are rost să compari mai departe — treci la poziția următoare.
Două bucle: i parcurge textul (de la 0 până la n-m, mai departe șablonul nu
mai încape), iar j parcurge șablonul. La prima diferență oprești bucla interioară
și avansezi i. Dacă j a ajuns la capătul șablonului fără diferențe, ai o
apariție la poziția i.
Algoritmul pas cu pas
Fie textul abracadabra (lungime n=11) și șablonul abra (lungime m=4).
Indexăm de la 0. Pozițiile de start posibile sunt i = 0, 1, ..., n-m = 7.
i=0: comparăm a=a, b=b, r=r, a=a — toate cele 4 coincid. Apariție la poziția 0.
a | b | r | a | c | a | d | a | b | r | a |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
i |
i=1: text[1]='b' vs sablon[0]='a' — nepotrivire din prima. Sărim.
i=2: text[2]='r' vs 'a' — nepotrivire. Sărim.
i=3: text[3]='a' se potrivește, dar text[4]='c' vs sablon[1]='b' — eșec la al doilea caracter.
i=4: text[4]='c' vs 'a' — nepotrivire.
i=5: text[5]='a' ok, dar text[6]='d' vs 'b' — eșec la al doilea.
i=6: text[6]='d' vs 'a' — nepotrivire.
Toate pozițiile i=1..6 eșuează repede — de obicei chiar la primul caracter.
i=7: comparăm a=a, b=b, r=r, a=a — toate coincid. Apariție la poziția 7.
a | b | r | a | c | a | d | a | b | r | a |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
i |
Șablonul abra apare pe pozițiile 0 și 7. Observă că i merge doar până
la n-m = 7: dacă ar porni mai departe, șablonul de 4 caractere ar ieși din text.
Implementare C++
Mai jos, căutarea naivă scrisă explicit cu două bucle. Afișează toate pozițiile de start unde apare șablonul.
#include <iostream>
#include <string>
using namespace std;
int main() {
string text = "abracadabra"; // n = 11
string sablon = "abra"; // m = 4
int n = text.size();
int m = sablon.size();
// i porneste doar pana la n-m: mai departe sablonul nu mai incape
for (int i = 0; i <= n - m; i++) {
int j = 0;
// compar caracter cu caracter; ies la prima nepotrivire
while (j < m && text[i + j] == sablon[j]) {
j++;
}
// daca am parcurs tot sablonul, e potrivire la pozitia i
if (j == m) {
cout << i << " "; // afiseaza: 0 7
}
}
return 0;
}Aceeași căutare, mult mai scurt, cu string::find într-o buclă. find întoarce
poziția primei apariții de la un punct dat, sau string::npos dacă nu mai există:
#include <iostream>
#include <string>
using namespace std;
int main() {
string text = "abracadabra";
string sablon = "abra";
// caut de la pozitia urmatoare dupa fiecare gasire
size_t poz = text.find(sablon, 0);
while (poz != string::npos) {
cout << poz << " "; // afiseaza: 0 7
poz = text.find(sablon, poz + 1);
}
return 0;
}Ambele variante afișează 0 7 — exact pozițiile găsite în trace.
Complexitate
| Caz | Timp | Spațiu |
|---|---|---|
| Mediu | O(n*m) | O(1) |
| Cel mai rău | O(n*m) | O(1) |
Pentru fiecare dintre cele n-m+1 poziții de start poți compara până la m
caractere, deci în cel mai rău caz aproximativ n*m comparații. Pe lângă cele
două șiruri de intrare nu folosim memorie suplimentară.
Trei capcane clasice la căutarea în șiruri:
- Depășirea capătului. Dacă lași
isă meargă până lan-1în loc den-m, accesezitext[i+j]în afara textului.itrebuie să se oprească lan-m, ca șablonul să încapă întreg. - Confuzia cu indexarea 0-based. Apariția la „poziția 0” înseamnă chiar începutul textului — e un rezultat valid, nu „lipsă potrivire”. Nu o ignora.
- Uitarea că
findîntoarcenpos.string::findîntoarcestring::npos(un număr foarte mare) când nu găsește, NU0. Verifică mereu cuif (poz != string::npos), altfel tratezi greșit cazul „negăsit”.