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