Introducere
Cercetarea datelor stocate în diferite structuri de date este o parte crucială a aproape fiecărei aplicații.
Există mulți algoritmi diferiți disponibili pentru a fi utilizați la căutare și fiecare dintre ei are implementări diferite și se bazează pe structuri de date diferite pentru a face treaba.
Abilitatea de a alege un algoritm specific pentru o anumită sarcină este o abilitate cheie pentru dezvoltatori și poate face diferența între o aplicație rapidă, fiabilă și stabilă și o aplicație care se prăbușește de la o simplă solicitare.
- Operatori de membru
- Cercetare liniară
- Cercetare binară
- Cercetare prin salt
- Cercetare Fibonacci
- Cercetare exponențială
- Cercetare prin interpolare
.
Operatori de apartenență
Algoritmii se dezvoltă și se optimizează în timp ca urmare a evoluției constante și a necesității de a găsi cele mai eficiente soluții pentru problemele de bază din diferite domenii.
Una dintre cele mai frecvente probleme din domeniul informaticii este căutarea într-o colecție și determinarea dacă un anumit obiect este prezent sau nu în colecție.
Chiar fiecare limbaj de programare are propria implementare a unui algoritm de căutare de bază, de obicei sub forma unei funcții care returnează o valoare Boolean
de True
sau False
atunci când se găsește un obiect într-o anumită colecție de obiecte.
În Python, cel mai simplu mod de a căuta un obiect este de a folosi Operatorii de apartenență – numiți astfel deoarece ne permit să determinăm dacă un anumit obiect este membru într-o colecție.
Aceste operatori pot fi folosiți cu orice structură de date iterabilă în Python, inclusiv Strings, Lists și Tuples.
-
in
– ReturneazăTrue
dacă elementul dat face parte din structură. -
not in
– ReturneazăTrue
dacă elementul dat nu face parte din structură.
>>> 'apple' in True>>> 't' in 'stackabuse'True>>> 'q' in 'stackabuse'False>>> 'q' not in 'stackabuse'True
Operatorii de apartenență sunt suficienți atunci când tot ce trebuie să facem este să aflăm dacă există o subșir în cadrul unui șir de caractere dat sau să determinăm dacă două Strings, Lists sau Tuples se intersectează în ceea ce privește obiectele pe care le dețin.
În cele mai multe cazuri avem nevoie de poziția elementului în secvență, pe lângă faptul că trebuie să determinăm dacă acesta există sau nu; operatorii de apartenență nu îndeplinesc această cerință.
Există mulți algoritmi de căutare care nu depind de operatorii încorporați și care pot fi utilizați pentru a căuta valori mai rapid și/sau mai eficient. În plus, aceștia pot produce mai multe informații, cum ar fi poziția elementului în colecție, în loc de a putea determina doar existența acestuia.
Cercetare liniară
Cercetarea liniară este unul dintre cei mai simpli algoritmi de căutare și cel mai ușor de înțeles. Ne putem gândi la el ca la o versiune accelerată a propriei noastre implementări a operatorului in
din Python.
Algoritmul constă în iterarea peste un array și returnarea indicelui primei apariții a unui element odată ce acesta este găsit:
def LinearSearch(lys, element): for i in range (len(lys)): if lys == element: return i return -1
Așa că dacă folosim funcția pentru a calcula:
>>> print(LinearSearch(, 2))
În urma executării codului, suntem întâmpinați cu:
1
Acesta este indexul primei apariții a elementului pe care îl căutăm – ținând cont de faptul că indexurile Python sunt bazate pe 0.
Complexitatea în timp a căutării liniare este O(n), ceea ce înseamnă că timpul de execuție crește odată cu numărul de elemente din lista noastră de intrare lys
.
Cercetarea liniară nu este adesea utilizată în practică, deoarece aceeași eficiență poate fi obținută prin utilizarea metodelor încorporate sau a operatorilor existenți, și nu este la fel de rapidă sau eficientă ca alți algoritmi de căutare.
Cercetarea liniară se potrivește bine atunci când trebuie să găsim prima apariție a unui element într-o colecție nesortată, deoarece, spre deosebire de majoritatea celorlalți algoritmi de căutare, nu necesită ca o colecție să fie sortată înainte de a începe căutarea.
Cercetare binară
Cercetarea binară urmează o metodologie de împărțire și cucerire. Este mai rapidă decât căutarea liniară, dar necesită ca array-ul să fie sortat înainte ca algoritmul să fie executat.
Să presupunem că căutăm o valoare val
într-un array sortat, algoritmul compară val
cu valoarea elementului din mijloc al array-ului, pe care îl vom numi mid
.
- Dacă
mid
este elementul pe care îl căutăm (cel mai bun caz), returnăm indicele acestuia. - Dacă nu, identificăm pe care parte a lui
mid
val
este mai probabil să se afle, pe baza faptului căval
este mai mică sau mai mare decâtmid
, și eliminăm cealaltă parte a tabloului. - Apoi urmăm recursiv sau iterativ aceiași pași, alegând o nouă valoare pentru
mid
, comparând-o cuval
și eliminând jumătate din posibilele potriviri în fiecare iterație a algoritmului.
Agoritmul de căutare binară poate fi scris fie recursiv, fie iterativ. Recursivitatea este, în general, mai lentă în Python, deoarece necesită alocarea de noi cadre de stivă.
Din moment ce un algoritm de căutare bun ar trebui să fie cât mai rapid și mai precis posibil, să luăm în considerare implementarea iterativă a căutării binare:
Dacă folosim funcția pentru a calcula:
>>> BinarySearch(, 20)
Obținem rezultatul:
1
Care este indicele valorii pe care o căutăm.
Acțiunea pe care algoritmul o execută în continuare în fiecare iterație este una dintre mai multe posibilități:
- Întoarcerea indicelui elementului curent
- Cercetarea prin jumătatea stângă a tabloului
- Cercetarea prin jumătatea dreaptă a tabloului
Noi putem alege doar o singură posibilitate pe iterație, iar grupul nostru de potriviri posibile se împarte la două în fiecare iterație. Acest lucru face ca complexitatea în timp a căutării binare să fie O(log n).
Un dezavantaj al căutării binare este că, în cazul în care există mai multe apariții ale unui element în matrice, aceasta nu returnează indexul primului element, ci mai degrabă indexul elementului cel mai apropiat de mijloc:
>>> print(BinarySearch(, 4))
Executarea acestei bucăți de cod va avea ca rezultat indicele elementului din mijloc:
1
Pentru comparație, efectuarea unei căutări liniare pe același array ar returna:
0
Ceea ce reprezintă indicele primului element. Cu toate acestea, nu putem spune în mod categoric că căutarea binară nu funcționează dacă un array conține același element de două ori – poate funcționa la fel ca și căutarea liniară și returnează prima apariție a elementului în unele cazuri.
Dacă efectuăm căutarea binară pe array-ul , de exemplu, și căutăm 4, am obține
3
ca rezultat.
Cercetarea binară este destul de frecvent utilizată în practică deoarece este eficientă și rapidă în comparație cu căutarea liniară. Cu toate acestea, are unele neajunsuri, cum ar fi dependența sa de operatorul //
. Există mulți alți algoritmi de căutare de tip “divide și cucerește” care derivă din căutarea binară, să examinăm în continuare câțiva dintre aceștia.
Jump Search
Jump Search este similar cu căutarea binară prin faptul că lucrează pe o matrice sortată și utilizează o abordare similară de tip “divide și cucerește” pentru a căuta în ea.
Poate fi clasificat ca o îmbunătățire a algoritmului de căutare liniară, deoarece depinde de căutarea liniară pentru a efectua comparația efectivă atunci când caută o valoare.
Dat fiind un tablou sortat, în loc să căutăm prin elementele tabloului în mod incremental, căutăm în salturi. Astfel, în lista noastră de intrare lys
, dacă avem o dimensiune a salturilor de salt, algoritmul nostru va lua în considerare elementele în ordinea lys
, lys
, lys
, lys
, lys
și așa mai departe.
Cu fiecare salt, stocăm valoarea anterioară pe care am căutat-o și indicele acesteia. Când găsim un set de valori în care lys
<element<lys
, efectuăm o căutare liniară cu lys
ca fiind elementul cel mai din stânga și lys
ca fiind elementul cel mai din dreapta din setul nostru de căutare:
Din moment ce acesta este un algoritm complex, să luăm în considerare calculul pas cu pas al căutării prin salt cu această intrare:
>>> print(JumpSearch(, 5))
- Cercetarea prin salt ar determina mai întâi mărimea săriturii prin calculul
math.sqrt(len(lys))
. Având în vedere că avem 9 elemente, dimensiunea saltului ar fi √9 = 3. - În continuare, calculăm valoarea variabilei
right
, care este minimul lungimii matricei minus 1, sau valoarea luileft+jump
, care în cazul nostru ar fi 0+3=3. Deoarece 3 este mai mic decât 8, folosim 3 ca valoare aright
. - Acum verificăm dacă elementul nostru de căutare, 5, se află între
lys
șilys
. Deoarece 5 nu este între 1 și 4, trecem mai departe. - În continuare, facem din nou calculele și verificăm dacă elementul nostru de căutare este între
lys
șilys
, unde 6 este 3+salt. Deoarece 5 este între 4 și 7, efectuăm o căutare liniară pe elementele cuprinse întrelys
șilys
și returnăm indicele elementului nostru ca fiind:
4
Complexitatea în timp a căutării prin salt este O(√n), unde √n este dimensiunea săriturii, iar n este lungimea listei, plasând căutarea prin salt între algoritmii de căutare liniară și căutare binară în ceea ce privește eficiența.
Cel mai important avantaj al căutării prin salt în comparație cu căutarea binară este că nu se bazează pe operatorul de împărțire (/
).
În cele mai multe unități centrale de procesare, utilizarea operatorului de împărțire este costisitoare în comparație cu alte operații aritmetice de bază (adunare, scădere și înmulțire), deoarece implementarea algoritmului de împărțire este iterativă.
Costul în sine este foarte mic, dar atunci când numărul de elemente de căutat este foarte mare, iar numărul de operații de împărțire pe care trebuie să le efectuăm crește, costul se poate aduna în mod incremental. Prin urmare, căutarea prin salt este mai bună decât căutarea binară atunci când există un număr mare de elemente într-un sistem în care chiar și o creștere mică a vitezei contează.
Pentru a face căutarea prin salt mai rapidă, am putea folosi căutarea binară sau o altă căutare internă prin salt pentru a căuta prin blocuri, în loc să ne bazăm pe căutarea liniară, mult mai lentă.
Cercetarea Fibonacci
Cercetarea Fibonacci este un alt algoritm de împărțire și cucerire care are asemănări atât cu căutarea binară, cât și cu căutarea prin salt. Își primește numele deoarece folosește numerele Fibonacci pentru a calcula dimensiunea blocului sau intervalul de căutare în fiecare pas.
Numerele Fibonacci încep cu zero și urmează modelul 0, 1, 1, 1, 2, 3, 3, 5, 8, 13, 21… unde fiecare element este suma celor două numere care îl precedă imediat.
Algoritmul lucrează cu trei numere Fibonacci la un moment dat. Să numim cele trei numere fibM
, fibM_minus_1
și fibM_minus_2
, unde fibM_minus_1
și fibM_minus_2
sunt cele două numere imediat înainte de fibM
în secvență:
fibM = fibM_minus_1 + fibM_minus_2
Inițializăm valorile la 0,1 și 1 sau la primele trei numere din secvența Fibonacci pentru a evita obținerea unei erori de indexare în cazul în care matricea noastră de căutare lys
conține un număr foarte mic de elemente.
Apoi alegem cel mai mic număr din secvența Fibonacci care este mai mare sau egal cu numărul de elemente din tabloul nostru de căutare lys
, ca valoare fibM
, și cele două numere Fibonacci imediat înainte de acesta ca valori fibM_minus_1
și fibM_minus_2
. În timp ce matricea are elemente rămase și valoarea lui fibM
este mai mare decât unu, noi:
- Comparăm
val
cu valoarea blocului din intervalul de până lafibM_minus_2
și returnăm indicele elementului dacă acesta se potrivește. - Dacă valoarea este mai mare decât elementul la care ne uităm în prezent, mutăm valorile lui
fibM
,fibM_minus_1
șifibM_minus_2
cu două trepte în jos în secvența Fibonacci și resetăm indicele la indicele elementului. - Dacă valoarea este mai mică decât elementul la care ne uităm în prezent, mutăm valorile lui
fibM
,fibM_minus_1
șifibM_minus_2
cu o treaptă în jos în secvența Fibonacci.
Să aruncăm o privire la implementarea Python a acestui algoritm:
Dacă folosim funcția FibonacciSearch pentru a calcula:
>>> print(FibonacciSearch(, 6))
Să aruncăm o privire la procesul pas cu pas al acestei căutări:
- Determinarea celui mai mic număr Fibonacci mai mare sau egal cu lungimea listei ca
fibM
; în acest caz, cel mai mic număr Fibonacci care îndeplinește cerințele noastre este 13. - Valorile ar fi atribuite ca:
- fibM = 13
- fibM_minus_1 = 8
- fibM_minus_2 = 5
- index = -1
- În continuare, verificăm elementul
lys
unde 4 este minimul lui -1+5 . Deoarece valoarea luilys
este 5, care este mai mică decât valoarea pe care o căutăm, mutăm numerele Fibonacci cu o treaptă mai jos în secvență, realizând valorile:- fibM = 8
- fibM_minus_1 = 5
- fibM_minus_2 = 3
- index = 4
- În continuare, verificăm elementul
lys
unde 7 este minimul lui 4+3. Deoarece valoarea luilys
este 8, care este mai mare decât valoarea pe care o căutăm, mutăm numerele Fibonacci cu două trepte mai jos în secvență.- fibM = 3
- fibM_minus_1 = 2
- fibM_minus_2 = 1
- index = 4
- Acum verificăm elementul
lys
unde 5 este minimul lui 4+1 . Valoarea luilys
este 6, care este valoarea pe care o căutăm!
Rezultatul, așa cum era de așteptat, este:
5
Complexitatea de timp pentru căutarea Fibonacci este O(log n); la fel ca pentru căutarea binară. Acest lucru înseamnă că algoritmul este mai rapid atât decât căutarea liniară, cât și căutarea prin salt în majoritatea cazurilor.
Cercetarea Fibonacci poate fi utilizată atunci când avem un număr foarte mare de elemente de căutat și dorim să reducem ineficiența asociată cu utilizarea unui algoritm care se bazează pe operatorul de împărțire.
Un avantaj suplimentar al utilizării căutării Fibonacci este acela că poate acomoda array-uri de intrare care sunt prea mari pentru a fi păstrate în memoria cache a procesorului sau în memoria RAM, deoarece caută prin elemente în trepte de mărime crescătoare, și nu într-o dimensiune fixă.
Cercetare exponențială
Cercetarea exponențială este un alt algoritm de căutare care poate fi implementat destul de simplu în Python, în comparație cu căutarea prin salt și căutarea Fibonacci, care sunt amândouă un pic mai complexe. Este cunoscut și sub numele de căutare galopantă, căutare dublă și căutare Struzik.
Cercetarea exponențială depinde de căutarea binară pentru a efectua comparația finală a valorilor. Algoritmul funcționează prin:
- Determinarea intervalului în care este probabil să se afle elementul pe care îl căutăm
- Utilizarea căutării binare pentru interval pentru a găsi indicele exact al elementului
Implementarea Python a algoritmului de căutare exponențială este:
Dacă folosim funcția pentru a găsi valoarea lui:
>>> print(ExponentialSearch(,3))
Algoritmul funcționează prin:
- Verificarea dacă primul element din listă se potrivește cu valoarea pe care o căutăm – din moment ce
lys
este 1 și căutăm 3, setăm indexul la 1 și mergem mai departe. - Cercetarea tuturor elementelor din listă și, în timp ce elementul de pe poziția indexului este mai mic sau egal cu valoarea noastră, creșterea exponențială a valorii lui
index
în multipli de doi:- index = 1,
lys
este 2, care este mai mic decât 3, astfel încât indicele este înmulțit cu 2 și setat la 2. - index = 2,
lys
este 3, care este egal cu 3, astfel încât indicele este înmulțit cu 2 și setat la 4. - index = 4,
lys
este 5, ceea ce este mai mare decât 3; bucla este întreruptă în acest punct.
- index = 1,
- Se efectuează apoi o căutare binară prin felierea listei;
arr
. În Python, acest lucru înseamnă că sublista va conține toate elementele până la al 4-lea element, așa că, de fapt, apelăm:
>>> BinarySearch(, 3)
care ar returna:
2
Care este indicele elementului pe care îl căutăm atât în lista originală, cât și în lista feliată pe care o transmitem algoritmului de căutare binară.
Cercetarea exponențială se execută în timp O(log i), unde i este indicele elementului pe care îl căutăm. În cel mai rău caz, complexitatea în timp este O(log n), atunci când ultimul element este elementul pe care îl căutăm (n fiind lungimea tabloului).
Cercetarea exponențială funcționează mai bine decât căutarea binară atunci când elementul pe care îl căutăm este mai aproape de începutul tabloului. În practică, folosim căutarea exponențială deoarece este unul dintre cei mai eficienți algoritmi de căutare pentru array-uri nemărginite sau infinite.
Cercetare prin interpolare
Cercetarea prin interpolare este un alt algoritm de împărțire și cucerire, similar cu căutarea binară. Spre deosebire de căutarea binară, acesta nu începe întotdeauna căutarea de la mijloc. Căutarea prin interpolare calculează poziția probabilă a elementului pe care îl căutăm folosind formula:
index = low + )*(high-low) / (lys-lys)]
În care variabilele sunt:
- lys – matricea noastră de intrare
- val – elementul pe care îl căutăm
- index – indicele probabil al elementului căutat. Acesta este calculat ca fiind o valoare mai mare atunci când val este mai aproape ca valoare de elementul de la sfârșitul tabloului (
lys
) și mai mică atunci când val este mai aproape ca valoare de elementul de la începutul tabloului (lys
) - low – indicele de început al tabloului
- high – ultimul indice al tabloului
Algoritmul caută prin calcularea valorii lui index
:
- Dacă se găsește o potrivire (când
lys == val
), se returnează indexul - Dacă valoarea lui
val
este mai mică decâtlys
, valoarea indexului este recalculată folosind formula pentru subregiul din stânga - Dacă valoarea lui
val
este mai mare decâtlys
, valoarea indicelui este recalculată folosind formula pentru sub-reagregatul din dreapta
Să continuăm și să implementăm căutarea prin interpolare folosind Python:
Dacă folosim funcția pentru a calcula:
>>> print(InterpolationSearch(, 6))
Valorile noastre inițiale ar fi:
- val = 6,
- low = 0,
- high = 7,
- lys = 1,
- lys = 8,
- index = 0 + = 5
Din moment ce lys
este 6, care este valoarea pe care o căutăm, ne oprim din execuție și returnăm rezultatul:
5
Dacă avem un număr mare de elemente, iar indexul nostru nu poate fi calculat într-o singură iterație, continuăm să recalculăm valorile pentru index după ce ajustăm valorile de high și low din formula noastră.
Complexitatea în timp a căutării prin interpolare este O(log log n) atunci când valorile sunt distribuite uniform. Dacă valorile nu sunt distribuite uniform, complexitatea timpului în cel mai rău caz este O(n), la fel ca în cazul căutării liniare.
Cercetarea prin interpolare funcționează cel mai bine pe array-uri sortate și distribuite uniform. În timp ce căutarea binară începe de la mijloc și întotdeauna se împarte în două, căutarea prin interpolare calculează poziția probabilă a elementului și verifică indicele, ceea ce face mai probabilă găsirea elementului într-un număr mai mic de iterații.
De ce să folosiți Python pentru căutare?
Python este foarte ușor de citit și eficient în comparație cu limbajele de programare mai vechi precum Java, Fortran, C, C++ etc. Un avantaj cheie al utilizării Python pentru implementarea algoritmilor de căutare este că nu trebuie să vă faceți griji cu privire la casting sau la tastarea explicită.
În Python, majoritatea algoritmilor de căutare pe care i-am discutat vor funcționa la fel de bine dacă căutăm un String. Rețineți că trebuie să facem modificări la cod pentru algoritmii care folosesc elementul de căutare pentru calcule numerice, cum ar fi algoritmul de căutare prin interpolare.
Python este, de asemenea, un loc bun pentru a începe dacă doriți să comparați performanța diferiților algoritmi de căutare pentru setul dvs. de date; construirea unui prototip în Python este mai ușoară și mai rapidă, deoarece puteți face mai multe cu mai puține linii de cod.
Pentru a compara performanța algoritmilor noștri de căutare implementați față de un set de date, putem folosi biblioteca de timp din Python:
import timestart = time.time()# call the function hereend = time.time()print(start-end)
Concluzie
Există multe moduri posibile de a căuta un element într-o colecție. În acest articol, am încercat să discutăm câțiva algoritmi de căutare și implementările lor în Python.
Alegerea algoritmului de utilizat se bazează pe datele prin care trebuie să căutați; array-ul de intrare, pe care l-am numit lys
în toate implementările noastre.
- Dacă doriți să căutați într-un tablou nesortat sau să găsiți prima apariție a unei variabile de căutare, cea mai bună opțiune este căutarea liniară.
- Dacă doriți să căutați într-un tablou sortat, există multe opțiuni dintre care cea mai simplă și mai rapidă metodă este căutarea binară.
- Dacă aveți un tablou sortat prin care doriți să căutați fără a utiliza operatorul de împărțire, puteți utiliza fie căutarea prin salt, fie căutarea Fibonacci.
- Dacă știți că elementul pe care îl căutați este probabil să fie mai aproape de începutul tabloului, puteți utiliza căutarea exponențială.
- Dacă array-ul dvs. sortat este, de asemenea, uniform distribuit, cel mai rapid și eficient algoritm de căutare de utilizat ar fi căutarea prin interpolare.
Dacă nu sunteți sigur ce algoritm să utilizați cu un array sortat, încercați fiecare dintre ei împreună cu biblioteca de timp Python și alegeți-l pe cel care se comportă cel mai bine cu setul dvs. de date.
.