Sunteți pe pagina 1din 49

Algoritmi, Structuri de date şi

Complexitate

Tema: Metode de căutare în tabele


ordonate şi neordonate

Ludmila NOVAC
Dr., conf. univ.
Dep. Informatică
Problema de căutare, aprecierea complexităţii
algoritmilor de căutare, viteza medie de căutare,
lungimea medie de căutare.
1. Tabele neordonate.
- Algoritmul căutării secvenţiale (metoda secvenţială de căutare).
- Metoda de căutare în Tabele neordonate structurate arborescent.
(căutarea în arborele binar de căutare)
2. Tabele ordonate. Tabele ordonate după cheie.
- Metoda binară de căutare (căutarea binară).
- Căutarea după metoda lui Fibonacci.
- Căutarea prin interpolare.
- Căutarea în Tabele ordonate (hash-tabele).
- Tabele ordonate după frecvenţa de adresare la înregistrări
tabelare.
2. Tabele ordonate.
Tabele ordonate după cheie.
- Metoda binară de căutare (căutarea binară).
- Căutarea după metoda lui Fibonacci.
- Căutarea prin interpolare.
- Căutarea în Tabele ordonate (hash-tabele).
- Tabele ordonate după frecvenţa de adresare
la înregistrări tabelare.
Căutarea binară
Presupunem că lista de elemente este ordonată crescător, K1 < K2 < ... < KN
şi se caută K in listă. Ideea algoritmului de căutare binară este de a testa întâi dacă
elementul K coincide cu elementul din mijloc al listei. Dacă da, se obţine o căutare cu
succes şi algoritmul se termina. În caz contrar, putem avea unul din următoarele cazuri:

Procedeul continuă pană când K este găsit sau până când K se caută într-o listă
vidă, deci K nu este găsit.

Lungimea căutării binare se obţine după formula


ce pentru n destul de mare aproape că este egal cu limita de jos teoretică pentru
metode de căutare, bazate numai pe compararea cheilor. Teoretic limita de jos
este egală cu log2(n+1). Căutarea binară este mult mai efectivă decât parcurgerea
secvenţială. Pentru n=1000, D1=500, iar D2=11.
Pentru a calcula lungimea medie practică de căutare în tabelul ordonat se poate scrie o
funcţie pentru a contoriza numărul operaţiilor de comparaţie.
Căutarea binară. Exemplu
a) Căutăm K=7:

se caută K=7 într-o listă vidă, deci K nu este găsit.

b) Căutăm K=21:
Algoritm de căutare binară
(varianta iterativă):
Presupunem K1 < K2 < ... < KN şi se caută cheia K în listă.
Folosim doi indici l, u pentru a preciza limitele între care se caută cheia K.
Iniţial l = 1 şi u = N. Folosim şi variabila de tip boolean gasit care este iniţial
falsă şi devine adevărată atunci când elementul a fost găsit.
Algoritm de căutare binară
(varianta recursivă):
Presupunem K1 < K2 < ... < KN şi se caută cheia K în listă.
Folosim doi indici l, u pentru a preciza limitele între care se caută cheia K.
Iniţial l = 1 şi u = N. Folosim şi variabila de tip boolean gasit care este iniţial
falsă şi devine adevărată atunci când elementul a fost găsit.
Analiza algoritmului:
• Operaţia principală care se efectuează este comparaţia dintre
elementul căutat şi elementele listei. Se observă că de fiecare dată
când se încearcă găsirea elementului se face comparaţia cu
elementul din mijlocul listei, după care, dacă nu este găsit se face
căutarea într-o listă de două ori mai mică.

• Să presupunem că N = 2k –1 atunci la prima trecere prin listă se


compară K cu elementul din mijloc al unei liste cu 2k –1 elemente,
la a doua trecere se compară K cu elementul din mijloc al unei liste
cu 2k-1 –1 elemente, ş.a.m.d. pană când, în cel mai rău caz, la a k-a
trecere se caută cheia K într-o listă cu 21-1 = elemente. Deci în cel
mai rău caz se efectuează k = log2(N+1) comparaţii.

• Dacă N este oarecare, atunci numărul maxim de comparaţii este


log2(N+1).
• Se poate arăta că algoritmul de căutare binară este O(log2(N+1)
Căutarea prin interpolare
Metoda de Căutarea prin interpolare
Este similară cu căutarea binară, dar foloseşte o altă formulă pentru calculul lui
"m", şi anume:
m=st+(x-v[st])*(dr-st)/(v[dr]-v[st])
ceea ce conduce la o delimitare mai rapidă a zonei din tablou în care s-ar
putea găsi x.

Ca principiu, metoda este inspirată după procedeul căutării într-o carte de


telefon.

• Această metodă este eficientă în cazul în care N este foarte mare


şi valorile elementelor tabloului au o distribuţie uniformă în intervalul
v[1],...,v[N]. Numărul de căutări în acest caz este de ordinul lg(lgN).

• Aplicarea căutării prin interpolare necesită ca elementul de căutat, x, să se


afle in interiorul intervalului v[1],..,v[N], altfel apare riscul ca valoarea
calculată a lui "m" să depăşească N.
Căutarea prin interpolare
Algoritm descris în pseudocod:

• Functia CăutareInterpolare(V,n,x)
st ← 0 dr ← n-1 gasit ← fals
dacă ((x<=v[dr]) şi (x>=v[st])) execută
repetă m ← st +(x-v[st])*[(dr-st)/(v[dr]-v[st])]
dacă x ≥ v[m] atunci
st ← m+ 1
altfel dr ← m-1
sfârşit dacă
până când ( (v[m]≠x) şi (st <dr)şi (v[st]=v[dr]) şi (x≥v[st]) şi (x≤v[dr]) )
sfârşit dacă
dacă v[m] =x atunci gasit ← adevarat
sfârşit dacă
sfârşit functie
Căutarea prin interpolare
• Implementare în C++
int CăutareInterpolare(int v[], int n, int x)
{
int st,dr,m;
st=0; dr=n-1;
if ((x<=v[dr]) && (x>=v[st]))
do
{
m=st+(x-v[st])*(dr-st)/(v[dr]-v[st]);
if(x>v[m]) st=m+1;
else dr=m-1;
} while((v[m]!=x) && (st=v[st]) && (x<=v[dr]));

if(v[m]==x) return 1;
else return 0;
}
Căutarea prin interpolare. Exemplu
Cheia este X=23

1) st=0, dr =7,
m=0+(23-1)*(7-0)/(38-1)=22*7/37=4,16 1 4 9 10 15 21 23 38
m=4 0 1 2 3 4 5 6 7

2) st=5, v[st]=21
m=5+(23-21)*(7-5)/(38-21)=5+2*2/17=5,235
1 4 9 10 15 21 23 38
m=5
0 1 2 3 4 5 6 7
3) st=6, v[st]=23
m=6+(23-23)*(7-6)/(38-23)=6+0*(15)=6
m=6 1 4 9 10 15 21 23 38
v[m]=23=Cheia x 0 1 2 3 4 5 6 7
Tabele ordonate

• Tabele pot fi ordonate crescător după codul numeric


al cheii, sau după frecvenţa apelurilor la
înregistrări.

• În primul caz pentru căutarea înregistrării de obicei


se foloseşte căutarea binară, dar în al doilea –
parcurgerea secvenţială.
Tabele ordonate după frecvenţa de adresare
la înregistrări tabelare
• Lungimea medie a parcurgerii secvenţiale în tabelul, ordonat
după frecvenţa apelărilor la înregistrări, depinde esenţial de
repartizarea frecvenţelor apelurilor şi se obţine după formula
generală:

• Dacă un număr relativ mic de înregistrări se căută foarte des,


atunci lungimea medie de căutare poate fi mult mai mică decât
la căutare binară.
Aceasta uneori se foloseşte la crearea tabelelor translatorului,
de exemplu tabelul de cuvinte cheie ale limbajului de intrare.
• Ordonarea tabelelor cere adăugător cheltuieli de timp al
calculatorului.
De aceea, tabelele ordonate se folosesc mai mult ca tabele
constante ale translatorului. Dar uneori se ordonează şi tabele
temporare, cu toate că ordonarea aceasta are anumite greutăţi.
Problema constă în aceea că tabelele temporare ce se
alcătuiesc în timpul translaţiei, în multe cazuri se folosesc şi
pentru căutare.
• Deja completate astfel de tabele au nevoie de verificare: oare
nu este inclusă înregistrarea dată în tabel la etapa precedentă
de lucru al translatorului? De aceea ordonarea tabelelor
temporare este necesar de făcut odată cu încărcarea lor.
• Pentru reducerea cheltuielilor timpului calculatorului la
ordonarea tabelelor temporare, uneori se foloseşte metoda
împărţirii, la care tabelul se împarte în compartimente,
corespunzătoare intervalelor diferite ale valorilor cheii.
Compartimentele sunt ordonate, iar înăuntru compartimentelor
înregistrările nu se ordonează. Pentru căutarea înregistrărilor
se foloseşte metoda combinată.
De exemplu, compartimentul se găseşte prin căutare binară, iar
înăuntru compartimentului se foloseşte parcurgerea
secvenţială.
Tabele cu adresare directă.
Funcţii de repartizare. Hash-funcţii
• Ne propunem să creăm o structură de date eficientă care să poată
face următoarele operaţii cât mai repede posibil: Inserează, Caută şi
Şterge. Ideea din spatele hashing-ului este memorarea unui element
într-un tablou sau listă, în funcţie de cheia sa.
• Pe cazul mediu toate aceste operaţii să necesite O(1) timp.

Să vedem cum:
• Elementele sunt puse într-un tablou alocat static pe poziţiile cheilor
lor. Prin adresare directă, un element cu cheia k va fi memorat în
locaţia k. Toate cele 3 operaţii sunt extrem de simple (necesită doar
o accesare de memorie), dar dezavantajul este că această tehnică
"mănâncă" foarte multă memorie: O(|U|), unde U este universul de
chei.
• hash (engl. hash = a toca, a fărâmiţa, tocătură).

• http://www.cs.ubbcluj.ro/~gabitr/Cursul12.pdf
Funcţiile hash
• Funcţiile hash (numite şi funcţii de dispersie sau funcţii de rezumat; din
englezescul hash functions) sunt, în ultimă instanţă, nişte funcţii matematice
destul de oarecare; ele sunt grupate sub umbrela funcţiilor hash mai
degrabă din punct de vedere al scopului urmărit de matematicieni decât din
punct de vedere al formei lor intrinseci.

Funcţia hash ideală este în mod simultan:


• universală: poţi să introduci orice parametru de intrare;
• repetabilă: de fiecare dată când introduci variabila X vei obţine acelaşi rezultat Y;
• unidirecţională: este uşor să calculezi valoarea funcţiei pe baza variabilei introduse
în funcţie (f(x) → x), dar imposibil să afli variabila pe baza valorii funcţiei (x →
f(x));
• biunivocă: ai garanţia că variabile diferite vor produce valori diferite ale funcţiei
(unicitate) – dar şi viceversa, ai garanţia că o valoare a funcţiei corespunde cu o
singură variabilă posibilă;
• concisă: produce rezultate cu o mărime predeterminată, indiferent de mărimea
variabilei de intrare.
Vom vedea în continuare că funcţia hash ideală este greu de găsit.
Deocamdată însă vom presupune că există o astfel de funcţie miraculoasă.

• Dar la ce folosesc de fapt funcţiile hash? Care sunt funcţiile astea?


Care sunt limitările lor? Să vedem...
Dar la ce folosesc de fapt funcţiile hash?
Care sunt funcţiile astea? Care sunt limitările lor?
În primul rând trebuie să ne întrebăm care este motivul pentru care ne-ar putea
interesa de fel funcţiile hash. Există două situaţii în care beneficiile utilizării
funcţiilor hash sunt evidente (deşi ele sunt de fapt folosite în mult mai multe
aplicaţii).
• Sume de control
Imaginaţi-vă că aţi descărcat un fişier uriaş de pe Internet şi vreţi să ştiţi
dacă fişierul care se află pe discul dumneavoastră este într-adevăr identic
cu originalul. O variantă ar fi să descărcaţi din nou acelaşi fişier şi să
comparaţi cele două variante bit cu bit. Soluţia pentru problemele acestui
caz este suma de control (în engleză „checksum”).
Dintre caracteristicile funcţiei hash ideale, sumele de control au nevoie în
special de repetabilitate, concizie şi unicitate.
• Securizarea datelor de acces (criptarea datelor)
Imaginaţi-vă că aveţi un magazin online care are clienţi înregistraţi; clienţii
se autentifică folosind adresa e-mail şi o parolă, iar pe baza acestor date de
identificare pot face cumpărături cu cartea de credit.
Pentru a evita situaţiile de acces la informaţie confidenţială, majoritatea
programatorilor aleg să nu stocheze parolele în clar, ci ca rezultat al unei
funcţii hash aplicate pe parola introdusă de utilizatori. În acest fel, chiar
dacă baza de date este compromisă, este totuşi dificil să fie aflate parolele
originale. Exemple: MD5, Familia SHA (SHA-0,…, SHA-3).
Dintre caracteristicile funcţiei hash ideale, securizarea datelor de acces are
nevoie în special de repetabilitate, unidirecţionalitate şi biunivocalitate.
http://www.capisci.ro/articole/Func%C5%A3ii_hash
Algoritmii de căutare care folosesc
funcţii hash
Algoritmii de căutare care folosesc funcţii hash constau din două părţi
distincte.
1. Prima parte este de a calcula o funcţie hash care
transformă cheia de căutare într-o adresă de tabel. In
mod ideal, chei diferite s-ar mapa la adrese diferite, dar
de multe ori două sau mai multe chei diferite pot fi
distribuite la aceeaşi adresă de tabel.
2. A doua parte a unui algoritm de căutare care foloseşte
hashing este un proces de rezolvare a coliziunilor. Una
din metodele de rezolvare a coliziunilor pe care o vom
studia utilizează liste înlănţuite, şi este, prin urmare,
imediat utilizabilă în situaţii dinamice în care numărul de
chei de căutare este dificil de prezis în avans.
Performanţa teoretică
Această aşteptare este performanţa teoretic optimă pentru orice
implementare a tabelei de simboluri, dar hashing-ul nu este un
panaceu universal, din două motive principale:
➢ timpul de rulare depinde de lungimea cheii, care poate fi o
responsabilitate în aplicaţii practice, cu chei lungi.
➢ hash nu oferă implementări eficiente pentru alte operaţii ale tabelei
de simboluri, cum ar fi select sau sort.
• Primul pas în a rezolva problema memoriei este de a folosi O(N)
memorie în loc de O(|U|), unde N este numărul de elemente
adăugate în hash. Ceea ce trebuie să abordăm este calculul funcţiei
de hashing, care transformă cheile în adrese ale tabelului.
• Astfel, un element cu cheia k nu va fi memorat în locaţia k, ci în h(k),
unde h: U -> {0, 1, ..., N-1} - o funcţie aleasă aleator, dar deterministă
(h(x) va returna mereu aceeaşi valoare pentru un anumit x în cursul
rulării unui program). Acest calcul aritmetic este în mod normal,
simplu de implementat, dar trebuie să se procedeze cu prudenţă,
pentru a evita diversele capcane subtile.
Tabele cu adresare directă. Funcţii de repartizare
Hash-funcţii
Fie un tabel de m înregistrări, toate înregistrările au diferite valori ale cheilor
k0, k1, k2,..., km-1, şi tabelul este reflectat în vectorul
T [0], T[1], …, T[n-1], unde m≤n.

Dacă este definită funcţia f(k), astfel, ca pentru orice ki, i=0, ..., m-1,
f(ki) are o valoare întreagă între 0 şi n-1, unde f(ki)≠f(kj), i≠j, atunci
înregistrarea tabelară cu cheia K se reflectă bireciproc în elementul T[f(K)].

Funcţia f(K) se numeşte funcţie de repartizare (dispersare sau


Hash funcţie). Această funcţie asigură calcularea pentru fiecare înregistrare
tabelară a numărului corespunzător al elementului vectorului T.
Accesul la înregistrare după cheia K se efectuează în acest caz nemijlocit prin
calcularea valorii f(K). Tabelele, pentru care există şi este cunoscută
(descrisă) funcţia de repartizare, se numesc tabele cu adresare directă.

Lungimea medie de căutare în astfel de tabele este minimală şi egală D3=1.


Tabele de repartizare (repartizarea aleatorie)
Noţiuni generale
• Deoarece, practic transformarea bireciprocă a cheii în adresa păstrării
înregistrării, în mod general, nu poate fi îndeplinită, atunci suntem nevoiţi
să renunţăm la cerinţa de reflectare birereciprocă.

• Aceasta aduce la suprapunerea înregistrărilor sau, altfel zis, la coliziuni.


Ca astfel de coliziuni să fie cât mai puţine, funcţia de repartizare se alege
din condiţia de reprezentare aleatorie şi cu cât se poate mai uniformă de
reflectarea cheilor în adresa de păstrare. Tabele construite după acest
principiu sunt numite tabele de repartizare.

• Repartizarea nu exclude pe deplin posibilitatea de suprapunere a


înregistrărilor (coliziuni). De acea se aplică diferite metode pentru
înlăturarea coliziunilor. Diferenţa variantelor de tabele de repartizare este
definită prin metoda folosită de înlăturare a coliziunilor.
Tabele de repartizare cu examinarea liniară
(adresare deschisă)
În metoda de repartizare cu examinarea liniară la reprezentarea tabelelor în vector de
lungime n se foloseşte următorul algoritm al inserării înregistrării cu cheia dată K:

1. Calculăm i=f(K). Trecem la punctul 2.


2. Dacă poziţia i este liberă, atunci înscriem în aceasta poziţie înregistrarea nouă.
În caz contrar trecem la punctul 3.
3. Fixăm i=(i+1)mod n, şi trecem la punctul 2:

• La includerea unei noi înregistrări algoritmul rămâne determinat până atunci, când
vectorul, în care se reflectă tabelul, conţine măcar o poziţie liberă.
• Dacă această condiţie nu se îndeplineşte este posibilă ciclarea, împotriva căreia
trebuie de luat măsuri speciale.
De exemplu, se poate introduce un contor de poziţii verificate, dacă acest număr a
devenit mai mare ca n, atunci algoritmul trebuie să fie oprit.
Căutarea înregistrărilor în tabele de repartizare
La căutarea înregistrării cu cheia dată K se foloseşte următorul algoritm:

1. Calculăm i=f(K). Trecem la punctul 2.


2. Dacă poziţia i este liberă atunci în tabelul dat nu există înregistrarea cu cheia
K. Dacă poziţia este ocupată şi cheia coincide cu cheia K, atunci căutarea
este reuşită, în caz contrar trecem la punctul 3.
3. Fixăm i=(i+1)mod n, şi trecem la punctul 2:

• La căutare algoritmul este determinat dacă tabelul conţine înregistrarea


tabelară cu cheia K, sau vectorul conţine poziţii libere. În cazul
neîndeplinirii acestor condiţii trebuie de luat măsuri speciale. De exemplu,
se poate introduce un contor de poziţii verificate, dacă acest număr a
devenit mai mare ca n, atunci algoritmul trebuie să fie oprit.
Exemplu de Hash funcţie
Declarăm în baza clasei usual_elem clasa hashing_elem care va fi dotată
cu o funcţie de repartizare.

// c l a s s "h a s h i n g _ e l e m"
class hashing_elem : public usual_elem
{
public:
hashing_elem(){ }

hashing_elem(char* init_name, int init_year, double init_salary):


usual_elem(init_name, init_year, init_salary) { }

int hf(int n) // hashing function


{
return (name[0]-'A')%n;
}
};
Exemplu Hash-tabel,
lungimea medie de căutare

A P –
B Q 11
C R 2
D S 4
E T –
F U 1
G V 2
H W 2
I X 1
J Y –
K Z –
L 1
M –
N 1
O

D_medie=(1+1+2+4+1+2+2+1+1+1)/10 =16/10=1,6
Analiza coliziunilor

• După cum se vede poziţiile 0, 5, 10, 11, 13 au rămas libere. Pe parcursul


completării tabelului în poziţiile 1, 2, 6, 7 au avut loc coliziuni. Drept vorbind,
coliziunea în poziţia 7 pentru înregistrarea tabelară cu cheia “White” este
secundară.

• Cercetările teoretice şi experimente pentru căutarea la metoda de repartizare cu


examinarea liniară au arătat, că pentru repartizarea aleatorie şi uniformă a
înregistrărilor prin funcţia de repartizare în intervalul [0, n-1], lungimea medie de
căutare nu depinde de lungimea tabelului, dar depinde numai de factorul de
încărcare

unde m - lungimea tabelului, iar n – lungimea vectorului de reprezentare.


• Această proprietate este foarte importantă, mai ales pentru tabele mari. Tabele
deterministe, atât ordonate cât şi cele ne ordonate nu posedă această proprietate. În
tabele deterministe lungimea medie de căutare creşte odată cu creşterea lungimii
tabelului.
Lungimea medie de căutare la metoda de repartizare
cu examinarea liniară
• Formula aproximativă

pentru lungimea medie de căutare la metoda de repartizare cu examinarea


liniară oferă coincidenţă suficientă cu experimentul pentru σ≤0,85.
• Formula este obţinută în presupunerea repartizării aleatorie şi uniformă a
înregistrărilor pe poziţiile vectorului de reprezentare.
Tabele de repartizare cu înlănţuirea externă
(repartizarea deschisă, înlănţuirea separată)

• În metoda examinării liniare înregistrările ce produc coliziuni se includ în poziţiile


libere ale aceluiaşi vector de reflectare. Însă pentru aceste înregistrări se poate crea
un tabel aparte.
• În tabelul adăugător înregistrările se pot lega în lanţ, precum în liste, pentru
uşurarea căutării.
• În tabele de repartizare cu înlănţuirea externă lungimea medie de căutare pentru
distribuirea uniformă şi aleatorie a înregistrărilor se defineşte după formula:

unde n – lungimea vectorului de reflectare, m – lungimea tabelului.


Tabele de repartizare cu înlănţuirea externă.
Exemplu

A P – 2
B Q 11 2
C R – 2
D S – –
E T – –
F U 11 –
G V – –
H W 1 –
I X – –
J Y – –
K Z 1 –
L – –
M 1 –
N –
O –
Tabele de repartizare cu înlănţuirea internă
Încărcarea poziţiilor vectorului de reflectare constă din două etape:
• prima etapă se aseamănă cu repartizarea prin înlănţuirea externă;
• a doua etapă se îndeplineşte după terminarea creării tabelului primar şi
celui secundar. Ea constă în mutarea lanţurilor din tabelul secundar în
poziţiile libere ale tabelului primar.
Astfel tabelele din exemplul precedent vor fi transformate în următorul tabel.

Prioritatea acestei metode în comparaţie cu precedenta – economisirea


memoriei, dar neajunsul – flexibilitatea mică şi algoritmul de încărcare a
tabelului este mai complicat.

• Lungimea medie de căutare aici se defineşte după aceiaşi formula:

această metodă se foloseşte pentru tabele permanente, şi pentru tabele


temporare, care se încărcă la prima etapă, dar se folosesc la alta.
Tabele de repartizare cu înlănţuirea internă. Exemplu

A P 2
B Q 1
C R 1
D S 2
E T 2
F U –
G V 1
H W 1
I X –
J Y 1
K Z –
L –
M 1
N –
O 1
Metode de rezolvare a coliziunilor
• Înlănţuire
• Liste statice
• Adresare deschisă
• Double hashing-ul lui Mihai Pătraşcu
Înlănţuire

• În fiecare poziţie din tabel ţinem o listă înlănţuită; insert, delete şi search
parcurg toată lista.
Pe un caz pur teoretic, toate cele N elemente ar putea fi repartizate în aceeaşi
locaţie, însă pe cazuri practice lungimea medie a celui mai lung lanţ este de
lg(N).
• Varianta: în loc de listă, de utilizat arbori.
Liste statice
• Varianta îmbunătăţită a metodei anterioare:

pentru că lungimea unui lanţ este cel mult lg(N), putem să


folosim, în loc de liste înlănţuite, vectori alocaţi dinamic de
lungime lg(N) - sau lg(N) + 3
se elimină pointerii.
Adresare deschisă
• Prin adresare deschisă, toate elementele sunt memorate în tabela
de dispersie. Pentru a realiza operaţiile cerute, verificăm succesiv
tabela de dispersie până când fie găsim o locaţie liberă (în cazul
Insert), fie găsim elementul căutat (pentru Caută, Şterge). Insă, în
loc să căutăm tabelă de dispersie în ordinea 0, 1, ..., N-1, şirul de
poziţii examinate depinde de cheia ce se inserează.
• Pentru a determina locaţiile corespunzătoare, extindem funcţia de
hashing astfel încât să conţină şi numărul de verificare ca un al
doilea parametru h: U * {0, 1, ..., N-1} -> {0, 1, ..., N-1}.
Astfel, când vom insera un element, verificăm mai întâi locaţia
h(k, 0), apoi h(k, 1) etc.
• Când ajungem să verificăm h(k, N) putem să ne oprim pentru că
tabelă de dispersie este plină. Pentru căutare aplicăm aceeaşi
metodă; dacă ajungem la h(k, N) sau la o poziţie goală, înseamnă
că elementul nu există. Ştergerile se fac însă mai greu, pentru că nu
se poate “şterge" pur și simplu un element deoarece ar strica toată
tabela de dispersie . In schimb, se marchează locaţia ce trebuie
ştearsă cu o valoare STERS şi se modifică funcţia Insert astfel încât
să vadă locaţiile cu valoarea STERS ca poziţii goale.
Adresare deschisă
Dispersie deschisă (sau înlănţuită)
Dimensiunea tabloului listelor de coliziuni este numărul total de valori de
dispersie MAX.
Lista elementelor cu aceeaşi valoare de dispersie:

Înlănţuirea coliziunilor in cazul dispersiei deschise.


Adresare
deschisă
Exemplu
Adresare deschisă

• Această implementare a unei tabele de dispersie


păstrează articolele într-o tabelă cu lungime dublă faţă
de numărul maxim de articole care se aşteaptă a fi
introduse, articole iniţializate cu valori nule (NULL
values).

• Pentru a insera un nou articol, îi calculăm poziţia în


tabelă, iar dacă este deja ocupată se caută spre dreapta
folosind macroul null pentru a testa dacă o poziţie este
ocupată. Pentru a căuta un articol cu o cheie dată îi
calculăm poziţia şi apoi scanăm pentru a găsi
concordanţa sau ne oprim dacă am ajuns la o poziţie
neocupată.
• Proprietatea 1: Înlănţuirea separată
reduce numărul de comparaţii pentru
căutare secvenţială cu un factor M (în
medie), folosind spaţiu suplimentar pentru
M înlănţuiri.
• Proprietatea 2: Într-o tabelă de dispersie
care este mai puţin de 2/3 plină adresarea
deschisă necesită mai puţin de 5 teste.
Double hashing-ul lui Mihai Pătrăşcu

O îmbunătăţire foarte mare la tabelă de dispersie este... încă o


tabelă de dispersie . Vom avea 2 tabele, fiecare cu propria ei funcţie
de hashing, iar coliziunile le rezolvăm prin înlănţuire; când inserăm
un element, îl vom adăuga în tabela în care intră într-un lanţ mai
scurt. Căutarea se face în ambele tabele în locaţiile returnate de cele
2 funcţii de hashing; ştergerea la fel.

Lungimea celui mai lung lanţ va fi, în medie, lg(lg(N)).


În practică, lungimea unui astfel de lanţ nu va depăşi 4 elemente,
pentru că cel mai mic N pentru care:

În loc de liste folosim vectori statici de dimensiune 4.


Funcţia de hashing depinde de tipul cheie
Strict vorbind , avem nevoie de o funcţie de hashing diferită pentru fiecare tip
de cheie , care ar putea fi folosită . Pentru eficienţă, în general se evită
conversia de tip explicită , străduindu-se în schimb pentru o întoarcere la
ideea de a considera reprezentarea binară a cheii într-un cuvânt maşină ca
un întreg pe care îl putem folosi pentru calcule aritmetice.

A fost o practică comună pe calculatoarele timpurii de a vedea o valoare de


cheie la un moment dat ca şir şi la altul ca un întreg. În unele limbaje de
nivel înalt este dificil să se scrie programe care depind de modul în care
cheile sunt reprezentate pe un anumit calculator, pentru că astfel de
programe, prin natura lor, sunt dependente de maşină şi, prin urmare, nu
sunt portabile. Funcţiile hash, în general, sunt dependente de procesul de
transformare a cheii în numere întregi, astfel încât independenţa şi eficienţa
maşinii sunt uneori dificil de realizat simultan în implementări de hashing.
Putem transforma de obicei chei întregi simple sau în virgulă flotantă, cu
doar o singură operaţie maşină, dar cheile de string-uri şi alte tipuri de chei
compuse necesită mai multă atenţie şi mai multă atenţie la eficienţă.

http://www.cs.ubbcluj.ro/~gabitr/Cursul12.pdf
Metode de transformare a cheilor:
• Variabilele de tip string pot fi transformate în numere în baza
256 prin înlocuirea fiecărui caracter cu codul său ASCII.

• Variabilele de tip dată se pot converti la întreg prin formula:


X = A * 366 + L * 31 + Z unde A, L şi Z sunt respectiv anul,
luna şi ziua datei considerate. De fapt, această funcţie
aproximează numărul de zile scurse de la începutul secolului I.

• Analog, variabilele de tip oră se pot converti la întreg cu


formula:
X = (H * 60 + M) * 60 + S unde H, M şi S sunt respectiv ora,
minutul şi secunda considerate, sau cu formula
X = ((H * 60 + M) * 60 + S) * 100 dacă se ţine cont şi de
sutimile de secundă. De data aceasta, funcţia este surjectivă
(oricărui număr întreg din intervalul 0 - 8.639.999 îi
corespunde în mod unic o oră).
In majoritatea cazurilor, datele sunt structuri care conţin
numere şi stringuri.
O bună metodă de conversie constă în alipirea tuturor acestor date şi
în convertirea la baza 256. Caracterele se convertesc prin simpla
înlocuire cu codul ASCII corespunzător, iar numerele prin convertirea
în baza 2 şi tăierea în "bucăţi" de câte opt biţi. Rezultă numere cu
multe cifre (prea multe chiar şi pentru tipul long long), care sunt supuse
unei operaţii de împărţire cu rest. Cum? De ce?
Exemplu simplificat:
Presupunem că avem o tabelă cu 101 poziţii şi cheia
A K E Y = 00001 01011 00101 11001 (Cod pe 5 biţi)=(44217)10 ≡80 (mod 101)
Baza 32 (semne) => A K E Y =
Dar dacă V E R Y L O N G K E Y =
1011000101100101100101100011110111000111010110010111001=
=
+12
=Horner
(((((((((22*32+5)32+18)32+25)32+12)32+15)32+14)32+7)32+11)32+5)32+25
Funcţii de dispersie foarte des folosite
1. Metoda împărţirii cu rest

Funcţia hash este: h(x) = x mod M, unde M


este numărul de intrări în tabelă.
Problema care se pune este să-l alegem pe M cât mai bine, astfel încât
numărul de coliziuni pentru oricare din intrări să fie cât mai mic.
De asemenea, trebuie ca M să fie cât mai mare, pentru ca media
numărului de chei repartizate la aceeaşi intrare să fie cât mai mică.
Totuşi, experienţa arată ca nu orice valoare a lui M este bună. Funcţiile
de hash întorc un număr între 0 şi M-1, unde M este dimensiunea
maximă a tabelei de hash.
Este recomandat ca M să fie ales un număr prim şi să se evite
alegerea lui

Motivul?
Exemplul 1
Presupunem că avem o tabelă cu 32 poziţii şi cheia
A K E Y = 00001 01011 00101 11001 (Cod pe 5 bits)=(44217)10 ≡80 (mod 101)
Baza 32 (semne) =>
AKEY=

• Dar dacă cheia este:


VERYLONGKEY=
1011000101100101100101100011110111000111010110010111001=
=
+12
• = Horner
(((((((((22*32+5)32+18)32+25)32+12)32+15)32+14)32+7)32+11)32+5)32+25
• valoarea mod32 ar fi întotdeauna valoarea ultimei litere din cheie.

Exemplul 2:
• Din aceleaşi motive, alegerea unei valori ca 1000 sau 2000 nu este prea
inspirată, deoarece ţine cont numai de ultimele 3-4 cifre ale reprezentării
zecimale.
Funcţii de dispersie foarte des folosite
2. Metoda înmulţirii
Funcţia hash este h(x) = [M * {x*A}] 0 < A < 1, iar prin {x*A}
se înţelege partea fracţionară a lui (x*A), adică (x*A - [x*A]).
• Exemplu:
dacă alegem M = 1234 şi A = 0.3, iar x = 1997, atunci avem
h(x) = [1234 * {599.1}] = [1234 * 0.1] = 123. Se observă că funcţia h
produce numere între 0 şi M-1.
Într-adevăr 0 ≤ {x*A} < 1 0 ≤ M * {x*A} < M. (Catalin Francu)
Observaţie: valoarea lui M nu mai are o mare importanţă.
M poate fi cât de mare ne convine, eventual o putere a lui 2.
În practică, s-a observat că dispersia este mai bună pentru unele valori ale
lui A şi mai proastă pentru altele;
• Donald Knuth propune valoarea
• Dacă funcţia aleasă are comportament cât mai apropiat de o
generare de numere aleatoare, elementele vor fi
"împrăştiate" în tabel în mod uniform. Pentru fiecare input,
fiecare ieşire ar trebui să fie într-un anumit sens, la fel de
probabilă. Ideal ar fi ca fiecare element să fie stocat singur în
locaţia lui. Acest lucru însă nu este posibil, pentru că N < |U|
şi, deci, de multe ori mai multe elemente vor fi repartizate în
aceeaşi locaţie.
• Acest fenomen se numeşte coliziune.

S-ar putea să vă placă și