Tabelle hash
In questa pagina 8
Definizione (tabella hash). La tabella hash è un'implementazione efficiente della mappa (vedi Mappe e dizionariADT mappa (chiavi distinte) con get, put, remove, keySet/values/entrySet; famiglia mappa/mappa ordinata/dizionario (multimappa); applicazioni; implementazioni semplici ma poco efficienti (lista, array indicizzato dalle chiavi); dizionario realizzato con una mappa di liste.Mappe e dizionari →) con tempo medio costante. Ha tre componenti:
- una funzione hash ( universo delle chiavi), composta da hash code e compression function ;
- un bucket array di capacità : contiene le entry con ;
- un metodo di gestione delle collisioni: con e .
Esempio. Con e la chiave va nel bucket : e , quindi il resto è .
L'idea: invece di usare la chiave come indice di un array enorme (Mappe e dizionariADT mappa (chiavi distinte) con get, put, remove, keySet/values/entrySet; famiglia mappa/mappa ordinata/dizionario (multimappa); applicazioni; implementazioni semplici ma poco efficienti (lista, array indicizzato dalle chiavi); dizionario realizzato con una mappa di liste.Mappe e dizionari →), la si trasforma in un indice piccolo. Visto che , la funzione non può essere iniettiva (principio dei cassetti: più chiavi di quante siano le celle), quindi le collisioni sono inevitabili e vanno gestite. Sono anche più frequenti di quanto si pensi, come nel paradosso del compleanno (Calcolo combinatorio per la probabilitàNegli spazi uniformi bisogna contare: principio di moltiplicazione, disposizioni con ripetizione n^k, disposizioni semplici n!/(n−k)!, permutazioni n!, combinazioni (n su k); estrazioni con e senza reinserimento e distribuzione ipergeometrica.Calcolo combinatorio per la probabilità →): con soli chiavi in bucket, la probabilità che nessuna collida è , cioè c'è una collisione con probabilità .
Cosa si chiede a una buona funzione hash
- deve imitare il più possibile un processo casuale (uniform hashing): per ogni e ogni vale e (le chiavi finiscono in indici statisticamente indipendenti: è la nozione di Indipendenza di eventiA e B sono indipendenti se P(A ∩ B) = P(A) P(B), cioè se sapere che uno si è verificato non cambia la probabilità dell'altro; l'indipendenza passa ai complementari, non va confusa con l'incompatibilità, e per più eventi va richiesta su ogni sottofamiglia.Indipendenza di eventi →, e il condizionamento è quello di Probabilità condizionataLa probabilità di A sapendo che si è verificato B è P(A ∣ B) = P(A ∩ B) / P(B), con P(B) > 0; è una nuova misura di probabilità, e da essa seguono la regola del prodotto e la regola della catena.Probabilità condizionata →; in pratica ogni chiave è assegnata a un bucket scelto a caso con probabilità uniforme );
- deve essere veloce da calcolare.
Hash code
- Numeri:
byte,short,char,intintper cast;floatFloat.floatToIntBits(k);long(int)((k >> 32) + (int)k)(somma dei 32 bit alti e bassi: un semplice cast aintperderebbe metà dei bit);doublecast aintdiDouble.doubleToLongBits(k)oppure, come perlong, somma delle due metà.Definizione (hash code per stringhe)
- Stringhe :
- somma dei caratteri : non è buona:
stop,spot,topshanno lo stesso hash code (le permutazioni collidono); - polinomiale: (con la regola di Horner); per parole inglesi vanno bene ( nella classe
Stringdi Java); - cyclic shift: si somma ogni carattere dopo uno shift ciclico di posizioni della somma parziale:
h <- s0; for i <- 1 to k-1: h <- (h << 5) | (h >> 27); h <- h + s_i.
- somma dei caratteri : non è buona:
- Stringhe :
Perché la somma dei caratteri è debole: l'addizione è commutativa, quindi qualsiasi permutazione delle stesse lettere dà lo stesso hash code (stop, spot, tops hanno gli stessi caratteri, il cui codice numerico dipende dalla codifica scelta: Codifiche binarie e informazione non numericaBit, byte e multipli (potenze di 2 e di 10); codici BCD e Gray; caratteri ASCII, Unicode e UTF-8; ordine dei byte (little e big endian); bit di parità e codice di Hamming per rilevare e correggere errori.Codifiche binarie e informazione non numerica →). Il polinomio usa invece potenze diverse di per le diverse posizioni, quindi lo scambio di due lettere cambia il risultato. La regola di Horner calcola il polinomio con moltiplicazioni e addizioni, senza elevare a potenza: h <- s0; for i <- 1 to k-1: h <- h*a + s_i. Per "abc" con : ; ; ; lo stesso valore di . Il costo è per una stringa di caratteri, quindi " costa " vale se la chiave ha lunghezza limitata. Gli operatori di shift <<, >> sono quelli sui bit (Aritmetica binariaSomma e sottrazione in binario, overflow per senza segno (riporto) e per complemento a 2 (segni), flag del processore, moltiplicazione per somme e scorrimenti, algoritmo di Booth, divisione, shift logici e aritmetici.Aritmetica binaria →, Sistemi di numerazione posizionaliNotazione posizionale in base b; conversioni tra base 10, 2, 8 e 16 per interi (divisioni successive) e per parti frazionarie (moltiplicazioni successive); numeri periodici in binario.Sistemi di numerazione posizionali →).
Esempio. Con , "ab" ha hash code polinomiale e "ba" ha , mentre con la somma dei caratteri entrambe valgono .
Compression function
Definizione (compression function: divisione e MAD)
- Metodo della divisione: . Se non è primo le correlazioni tra gli hash code tendono a conservarsi; ad esempio con conta solo gli ultimi bit e con solo le ultime cifre decimali. In pratica si sceglie primo e lontano da potenze di .
- MAD (multiply-add-divide): con primo e scelti a caso, .
Perché potenza di è una cattiva scelta. Scrivere un numero in binario (Sistemi di numerazione posizionaliNotazione posizionale in base b; conversioni tra base 10, 2, 8 e 16 per interi (divisioni successive) e per parti frazionarie (moltiplicazioni successive); numeri periodici in binario.Sistemi di numerazione posizionali →): sono esattamente gli ultimi bit di , quindi tutti i codici che hanno gli stessi ultimi bit collidono. Con i codici vanno tutti nel bucket ; con vanno nei bucket . Lo stesso accade con e le ultime cifre decimali. Un primo non ha divisori in comune con la maggior parte degli schemi regolari dei dati, e li "mescola".
Esempio. Con l'hash code dà ; con MAD (, , ) l'hash code dà .
Separate chaining
Definizione (load factor e separate chaining). Ogni bucket è una mappa più piccola, realizzata con una lista. Il load factor è ( entry, bucket): la lunghezza media di un bucket.
Esempio. Con entry e bucket: . Perché è la lunghezza media: con entry distribuite sui bucket in modo uniforme, il numero di entry nel bucket ha Valore attesoIl valore atteso E[X] = Σ x p_X(x) è la media dei valori di X pesata con le loro probabilità (esiste se la serie converge assolutamente); per una funzione g vale E[g(X)] = Σ g(x) p_X(x) senza trovare la legge di g(X), ed E è lineare: E[aX + bY + c] = aE[X] + bE[Y] + c.Valore atteso → (ogni entry cade nel bucket con probabilità , e il valore atteso di una somma è la somma dei valori attesi).
get(k): se esiste (k, x) in A[h(k)] restituisci x, altrimenti null
put(k, v): se esiste (k, x) in A[h(k)] sostituisci x con v e restituisci x
altrimenti inserisci (k, v) in coda ad A[h(k)], n <- n + 1, restituisci null
remove(k): se esiste (k, x) in A[h(k)] toglila, n <- n - 1, restituisci x; altrimenti nullSi assume che il calcolo di costi .
Proprietà (costo di una tabella hash: caso pessimo e medio). Per una tabella con entry
get,put,removecostano , dominate dalla ricerca della chiave nel bucket: banale; con tutte le entry nello stesso bucket e chiave cercata assente.Caso medio. Sotto l'ipotesi di uniform hashing la complessità media è , nel senso: ricerca senza successo, media su tutti i valori di equiprobabili; ricerca con successo, media su tutte le chiavi presenti equiprobabili inserite una dopo l'altra senza cancellazioni. In pratica si impone . Il load factor è il compromesso tra spazio e velocità: piccolo = tempo basso e spazio elevato; grande = tempo elevato e spazio ridotto. La qualità di determina quanto ci si avvicina all'ipotesi ideale.
Esempio. Con una ricerca senza successo costa in media circa confronti, mentre nel caso pessimo, con tutte le entry in un bucket, costa .
Da dove viene . Ricerca senza successo di : si calcola (costo costante, il "") e si confronta con tutte le entry del bucket , in media : totale . Ricerca con successo: la chiave cercata è stata inserita come -esima; quando è stata inserita nel suo bucket c'erano in media altre entry davanti a lei (inserimento in coda), quindi ci vogliono in media confronti. Facendo la media sulle chiavi equiprobabili: (si usa la somma dei primi interi, SommatorieIl simbolo di sommatoria, le sue proprietà (linearità, additività, cambio di indice) e le somme notevoli di Gauss e geometrica.Sommatorie →). Il grafico mostra che finché resta costante (per esempio sotto la soglia ) il costo medio è una costante piccola, indipendente da .
Grafico interattivo: Confronti medi per una ricerca in una tabella hash con separate chaining in funzione del load factor: senza successo 1 + λ, con successo circa 1 + λ/2; a λ = 0,75 (soglia di rehashing) sono 1,75 e 1,375
Rehashing
Definizione (rehashing). Quando supera una soglia (ad esempio ):
- si crea un nuovo bucket array di capacità (se serve, si cerca un primo );
- si sceglie una nuova funzione hash;
- si reinseriscono tutte le entry.
Esempio. Con e soglia il nono inserimento () causa il rehashing verso , per esempio il primo .
La condizione ammortizza il costo del rehashing sugli inserimenti che lo hanno preceduto: è lo stesso argomento dell'array estendibile che raddoppia (Liste, pile e codeRipasso degli ADT elementari: lista index-based (array) e position-based (lista doppiamente concatenata con sentinelle), pila LIFO, coda FIFO, deque, iteratori; interfacce, implementazioni e costi; array estendibile e coda circolare.Liste, pile e code →). Dopo il rehashing con e si ha , e si può arrivare a entry senza nuovo rehashing (), mentre con si supera la soglia (): servono inserimenti prima del prossimo rehashing, quindi il costo del trasferimento è diviso su molti inserimenti. Il trasferimento di entry costa perché le chiavi sono già distinte e non occorre cercarle prima di inserirle.
Esempio svolto
, , separate chaining, chiavi inserite nell'ordine :
| 12 | 44 | 13 | 88 | 23 | 94 | 11 | 39 | 20 | |
|---|---|---|---|---|---|---|---|---|---|
| 41 | 137 | 44 | 269 | 74 | 287 | 38 | 122 | 65 | |
| 8 | 5 | 0 | 5 | 8 | 1 | 5 | 1 | 10 |
Contenuto finale (bucket vuoti omessi): ; ; ; ; . Le collisioni sono nei bucket 1, 5 e 8. Load factor . La ricerca di confronta , , : tre confronti. Passaggi per : e , quindi il resto è e va in ; per : , anche in , dietro a (inserimento in coda). Lunghezze dei bucket: (somma , uguale a ); gli altri bucket sono vuoti.
Pro e contro
Pro: implementazione facile; ottime prestazioni al caso medio; non richiede un universo ordinato. Contro: complessità elevata al caso pessimo; esito dipendente dalla bontà della funzione hash; spreco di spazio per mantenere basso.
Errori comuni
- Dire che
getè al caso pessimo: lo è in media, con costante. - Scegliere potenza di o di con la divisione.
- Dimenticare che dopo il rehashing la funzione hash cambia: ogni entry va reinserita, non copiata nella stessa posizione.
- Usare la somma dei caratteri come hash code di stringhe: gli anagrammi collidono.
Versione ripasso
- Definizione: implementazione della mappa (vedi Mappe e dizionariADT mappa (chiavi distinte) con get, put, remove, keySet/values/entrySet; famiglia mappa/mappa ordinata/dizionario (multimappa); applicazioni; implementazioni semplici ma poco efficienti (lista, array indicizzato dalle chiavi); dizionario realizzato con una mappa di liste.Mappe e dizionari →) con tempo medio costante. Tre componenti: funzione hash (hash code seguito da una compression function); bucket array di capacità , con che contiene le entry con ; gestione delle collisioni ( con ).
- Esempio: con e la chiave dà , quindi bucket .
- Collisioni inevitabili: , quindi non è iniettiva (principio dei cassetti). Con chiavi in bucket, la probabilità che nessuna collida è (vedi Calcolo combinatorio per la probabilitàNegli spazi uniformi bisogna contare: principio di moltiplicazione, disposizioni con ripetizione n^k, disposizioni semplici n!/(n−k)!, permutazioni n!, combinazioni (n su k); estrazioni con e senza reinserimento e distribuzione ipergeometrica.Calcolo combinatorio per la probabilità →).
- Buona funzione hash: (1) uniform hashing, cioè e indipendenza tra chiavi diverse (vedi Indipendenza di eventiA e B sono indipendenti se P(A ∩ B) = P(A) P(B), cioè se sapere che uno si è verificato non cambia la probabilità dell'altro; l'indipendenza passa ai complementari, non va confusa con l'incompatibilità, e per più eventi va richiesta su ogni sottofamiglia.Indipendenza di eventi →); (2) velocità di calcolo.
- Hash code per i numeri:
intcosì com'è;longsommando le due metà a bit (un cast diretto perderebbe metà dei bit);floatedoubletramite la rappresentazione in bit. - Hash code per stringhe:
- la somma dei caratteri è cattiva:
stop,spot,topscollidono; - polinomiale con , calcolato con Horner (
h <- h*a + s_i): per"abc"si ottiene ; - cyclic shift di bit sulla somma parziale, poi somma del carattere successivo.
- Esempio:
"ab"dà ,"ba"dà ; con la somma entrambe valgono .
- la somma dei caratteri è cattiva:
- Compression, divisione: con primo e lontano da potenze di : con i codici finiscono tutti nel bucket . Esempio: con , .
- Compression, MAD: , con primo, scelti a caso e . Esempio: , , , con hash code dà bucket .
- Separate chaining: ogni bucket è una lista. Load factor , cioè la lunghezza media di un bucket; con e è .
get,puteremovescorrono la lista del bucket ;putaggiunge in coda. - Costo: caso pessimo , con tutte le entry nello stesso bucket. Caso medio sotto uniform hashing ; in pratica si tiene .
- Ricerca senza successo: in media confronti.
- Ricerca con successo: in media , mediando sulle chiavi inserite in coda.
- Rehashing: se supera una soglia (ad esempio ) si crea un array di capacità (dopo , ad esempio ), si sceglie una nuova e si reinseriscono tutte le entry: , ammortizzato sugli inserimenti precedenti.
- Esempio svolto: , , chiavi danno bucket . Contenuto: , , , , ; la ricerca di fa confronti.
- Pro/contro: semplice e veloce in media, non richiede un universo ordinato; caso pessimo lineare, dipende dalla qualità di , spreco di spazio per tenere basso.
- Errori tipici: dire che
getè nel caso pessimo; scegliere potenza di o di ; copiare le entry invece di reinserirle dopo il rehashing; usare la somma dei caratteri come hash di una stringa.
Esercizi su questo argomento
- Esercizio 19 · tabelle hash con separate chaining
- Esercizio 20 · duplicati vicini con una mappa
- Esercizio - Confronto tra map e vector (secondo appello 2021-22, teoria)
- Esercizio - Famiglia universale mod p mod m - verifica esaustiva e controesempio
- Esercizio - Liste di collisione con hash universale e dimensionamento della tabella
- Esercizio - Union bound e inclusione-esclusione, due dadi e collisioni di una funzione hash
Teoria collegata
- SQL avanzato - indici, transazioni e trigger
- Mappe e dizionari
- Rappresentazione dei grafi
- Funzioni hash e crittografia simmetrica
- Hashing randomizzato - famiglie universali, filtri di Bloom e fingerprint
- Palline e urne - compleanno, carico massimo e approssimazione di Poisson
- Union bound e verifica randomizzata - identità polinomiali e prodotti di matrici
- File system - interfaccia, directory e implementazione
- Memoria centrale - allocazione contigua, paginazione e segmentazione