Note su tabelle hash per ASD 2010-11 (DRAFT)
Nicola Rebagliati
17 dicembre 2010
1
Dizionari
Supponiamo di voler gestire un insieme di elementi indicizzati da chiavi con
una struttura ad accesso rapido, ad esempio costante. Osservate che liste ed
alberi binari di ricerca richiederebbero, nella loro operazione più costosa, una
complessità di O(n) e O(log n) rispettivamente. Supponiamo di gestire coppie
(e,k), con k proveniente da un insieme universo U , k ∈ U .
Possiamo implementare un dizionario ad accesso diretto che si appoggia su
un vettore T:
INSERT T[k] ← (e, k)
DELETE T[k] ← N IL
SEARCH return T[k]
Ora, quale possibile problema scorgete da questa implementazione? Se considerate un esempio risulta subito chiaro che lo spazio in memoria necessario sia
O(|U |), la cardinalità dell’universo delle chiavi. Quest’ultimo potrebbe essere
molto più grande delle chiavi usate in pratica. Per misurare l’efficienza di un
dizionario usiamo il concetto di fattore di carico:
n := chiavi effettivamente usate
m := chiavi disponibili nel dizionario
In linea teorica vorremmo avere α = 1, perché α > 1 implica che potremmo
avere delle inconsistenze e α < 1 degli sprechi di spazio.
α=
2
Tabelle hash
Le tabelle di hash sono progettate per avere un fattore di carico α ≥ 1, l’idea
è di mappare l’insieme delle chiavi in un insieme di numeri naturali molto più
piccolo:
h : U → [0, . . . , m − 1]
le operazioni del dizionario diventano:
1
INSERT T[h(k)] ← (e, k)
DELETE T[h(k)] ← N IL
SEARCH return T[h(k)]
3
Esempio
Prima di entrare nel merito di alcune problematiche principali delle tabelle di
hash, consideriamo un esempio. Abbiamo un insieme di libri sugli animali,
ognuno dei quali ha come chiave il nome dell’animale a cui si riferisce. Per
semplicità consideriamo nomi di animali con 5 o meno lettere (nel caso delle
lettere mancanti aggiungiamo uno spazio). Se implementiamo un dizionario
che contempli ogni chiave possibile avremmo m = 215 , un numero molto più
grande del numero di stringhe di animali! Per questo ricorriamo alle tabelle
hash usando un dizionario con soli 13 elementi.
Ad ogni lettera assegnamo con la funzione ] : Σ → [0, 21] il suo numero
d’ordine nell’alfabeto a 21 lettere. Lo zero viene assegnato allo spazio. Se
abbiamo una chiave s := s1 s2 s3 s4 s5 , la sua funzione di hash è:
!
5
X
h(s) =
](si ) mod 13
i=1
Quindi inseriamo la chiave ‘ CANE’ in posizione h( CANE) = (0 + 3 +
1 + 12 + 15)mod 13 = 8, h(GATTO) = (7 + 1 + 18 + 18 + 13)mod 13 = 5 e
h(LEONE) = (10 + 5 + 13 + 12 + 5)mod 13 = 6. La situazione dell’array è la
seguente:
(0 : N IL, 1 : N IL, 2 : N IL, 3 : N IL, 4 : N IL,
5 : GAT T O, 6 : LEON E, 7 : N IL, 8 : CAN E,
(1)
9 : N IL, 10 : N IL, 11 : N IL, 12 : N IL)
Ora inseriamo la chiave ‘ZEBRA’. Purtroppo però il suo hash (h(ZEBRA) =
6) fornisce un indirizzo già occupato! Questa si dice una collisione. Gestire le
collisioni si può fare con le liste concatenate o l’indirizzamento aperto.
4
Alcune osservazioni sulle funzioni di hash
• m < |U |, in tal caso le collisioni sono inevitabili
• Chiavi simili devono avere hash diversi (euristicamente perché gli insiemi
di chiavi usati in pratica hanno molte similarità)
• Se m = |U |, ∀u, v, .u 6= v → h(u) 6= h(v) si dice hash perfetta. Di solito
non è il caso che consideriamo
2
• Parliamo di hashing uniforme quando:
∀i.
numero di chiavi k tale che h(k) == i
1
=
n
m
• Studiare l’uniformità delle funzioni di hash è di solito difficile.
5
Liste concatenate
Un modo per risolvere il problema delle collisioni è la creazione di liste puntate
associate al codominio della funzione di hash. In tal caso la collisione si risolve
aggiungendo l’elemento alla lista.
INSERT Inserisci (x, k) in testa alla lista T[h(k)] ← (e, k). Complessità O(1).
DELETE Cancella (x, k) dalla lista T[h(k)]
SEARCH Ricerca k nella lista T[h(k)]
6
Indirizzamento Aperto
L’indirizzamento aperto non utilizza strutture dati ausiliarie come le liste, piuttosto se un elemento non può essere inserito a causa di una collisione, un altro
posto libero viene cercato dove inserirlo. Innanzitutto estendiamo la funzione
di hash:
h : U × [0, . . . , m − 1] → [0, . . . , m − 1]
for j = 0 : m − 1 do
if T [h(k, j)] == N IL or T [h(k, j)] == DELET ED then
T [h(k, j)] ← k
end if
end for
INSERT
1
Complessità O( 1−α
).
DELETE
T [h(k)] ← DELET ED
SEARCH
for j = 0 : m − 1 do
if T [h(k, j)] == k then
return h(k, j)
else
T [h(k, j)] == N IL
return N IL
end if
end for
Complessità: se l’elemento viene trovato è O( α1 log
1
)
trovato è O( 1−α
3
1
1−α ),
se non viene
Le funzioni h possono essere:
0
Ispezione lineare h(k, i) = (h (k) + i)mod m
0
Ispezione quadratica h(k, i) = (h (k) + c1 i + c1 i2 )mod m
0
Doppio hashing h(k, i) = (h (k) + i)mod m, dove h2 (k) ed m devono essere
primi tra loro
Una proprietà necessaria di h è che il vettore di numeri h(k, 0), h(k, 1), . . . , h(k, m−
1) sia un permutazione di 0, 1, . . . , m − 1
Le complessità che abbiamo proposto valgono per il doppio hashing. Non
vediamo le dimostrazioni, per chi fosse interessato si trovano: pg.186 CLRS in
italiano, oppure 221 CLRS in inglese.
7
Altre osservazioni
Supponiamo che in pratica il numero di chiavi usate sia molto inferiore rispetto
al numero di chiavi possibili della funzione di hash, abbiamo qualche garanzia
che non possano succedere collisioni? La risposta è no, ad esempio se le chiavi
fossero 23 e gli spazi possibili 365, avremmo che la probabilità
che una coppia
1
. Dal momento che le coppie sono 23
=
253
la probabilità
non collida sia 1− 365
2
253
che non ci siano collisioni è 364
≤
50%
365
Le tabelle hash hanno altri utilizzi oltre la creazione di dizionari efficienti.
Spesso sono usate per verificare l’integrità di un file, per controllare se due
oggetti sono uguali o per generare un’unica chiave a partire da un elemento.
E’ molto usata la funzione MD5, che presa una stringa qualunque restituisce
un numero binario a 128 bit. Viste tutte le considerazioni fatte sulle collisioni
questi utilizzi non sono giustificati: un attaccante potrebbe facilmente creare
due file con lo stesso MD5 e simulare il segreto di un altro utente.
8
Bibliografia
Mi aspetto che leggiate il cap. 11 del Cormen Leiserson Rivest Stein o che
approfondiate indipendentemente l’argomento.
4