Necesitatea tabelelor de dispersie / hashtables
Preliminarii
Implementarea tabelelor de dispersie
Functia hash
Exemple de functii hash
Strategii de rezolvare a coliziunilor
Sa presupunem ca avem de rezolvat urmatoarea problema:
La supermarket avem |K| = 3000 clienti care vor sa cumpere hartie igienica.
Fiecare client e limitat la 3 pachete.
Fiecare client e identificat prin CNP.
Restrictii:
Nu dispunem de suficienta memorie pentru un vector de frecvente.
Dorim solutii o(log |K|) -> cresterea functiei trebuie sa fie mai lenta ca log |K| -> adica mai rapid decat cautarea binara.
Ideea este ca:
Dorim o structura eficienta pentru a stoca si accesa asocieri de tipul (cheie -> valoare) cu cheile arbitrar de mari.
Avem o multime de chei K pe care dorim s-o inseram in tabelul de dispersie.
In exemplul anterior K = {1970502301921 , 2910410250734, ...}, iar |K|=3000.
Cheile se vor insera impreuna cu niste valori arbitrare asociate (cheie1,valoare1), (cheie2,valoare2).
In exemplul anterior valoarea asociata este numarul de articole cumparate.
Se noteaza universul cheilor U (cu K ⊂ U), multimea in care se pot genera chei.
In exemplul anterior U este multimea CNPurilor corecte.
Avem la dispozitie un tabel de dimensiune n > |K|.
Putem defini un factor de umplere α = p/n, unde p este numarul de intrari ocupate, iar n este numarul maxim de intrari.
Pentru a implementa un tabel de dispersie avem nevoie de:
0. Un tabel T cu n > |K| intrari.
O functie H care mapeaza cheile pe setul de indici din tabelul de dispersie astfel:
indice = H(cheie) % n, astfel incat T[indice] sa contina (cheie, valoare)
O strategie de rezolvare a conflictelor
O functie hash performanta trebuie sa respecte urmatoarele proprietati:
Sa poata fi calculata rapid.
Sa minimizeze numarul de coliziuni.
cat mai surjectiva (dorim ca imaginea lui H(cheie)%n sa umple setul de indici)
cat mai injectiva (dorim ca chei diferite sa produca indici diferiti)
Sa aiba imaginea cat mai mica (dorim sa minimizam dimensiunea tabelului)
Daca se cunoaste dinainte multimea de chei, se poate precalcula o functie hash fara coliziuni. O astfel de functie se numeste functie hash perfecta.
O functie hash perfecta minimala este o bijectie care minimizeaza dimensiunea imaginii astfel incat numarul de intrari n din tabelul de dispersie este egal cu numarul de chei |K|.
1. Metoda diviziunii
index = H(k) = k % d.
d este ales ca fiind un numar prim apropiat de dimensiunea tabelului (care dimensiune este de obicei putere de doi)
2. Metoda inmultirii
index = H(k) = floor( n * (k*A mod 1) )
A este un numar real cu care multiplicam cheia k;
luam partea fractionara a numarului real rezultat ∈ [0,1)
o inmultim cu dimensiunea tabelului n
partea intreaga a rezultatului este un indice ∈ {0,1,2, ..., n-1}
3. Metoda impachetarii
impartim cheia k in bucati de dimensiune egala
adunam/xoram bucatile
Din cauza ca dimensiunea tabelelor de dispersie este de obicei putere de doi e mai eficient sa impartim cheia in bucati de biti consecutivi.
De exemplu daca avem un tabel de dimensiune 16, observam ca indicii {0,...,15} sunt toate valorile care se pot reprezenta pe 4 biti (16=2^4).
Putem sa impartim cheia in bucati de 4 biti si sa le xor-am.
Prin inlantuire (chaining)
cu liste
cu arbori binari de cautare
cu arbori binari de cautare echilibrati
cu alte tabele de dispersie 😎
Open addressing
Linear probing: index = h(k) + i
Quadratic probing: index = h(k) + i*i
Double hashing: index = h(k) + i* h2(k)
Coalesced hashing
Cuckoo hashing
etc.
Elementul hashuit in locatia h se plaseaza la inceputul listei h
Elementul hashuit in locatia i se plaseaza in acelasi array, in urmatoarea locatie libera h+1, h+2, h+3, h+4
Elementul hashuit in locatia i se plaseaza in acelasi array, in urmatoarea locatie libera h+1, h+4, h+9, h+16