Tabelle hash
In questa pagina 8
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 .
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);
- 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à. - 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:
Compression function
- 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, .
Separate chaining
Ogni bucket è una mappa più piccola, realizzata con una lista. Il load factor è ( entry, bucket): la lunghezza media di un bucket.
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 .
Caso pessimo. Per una tabella con entry get, put, remove costano , 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.
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.
La condizione ammortizza il costo del rehashing sugli inserimenti che lo hanno preceduto. 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.
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
- Componenti: funzione hash = hash code + compression function ; bucket array di capacità ; gestione delle collisioni ( con ). Realizza la 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 →).
- Obiettivi di : uniform hashing (, indipendenza) e velocità.
- Hash code: numeri per cast o somma di metà alte e basse; stringhe: somma (cattiva:
stop,spot,topscollidono), polinomiale ( in Java), cyclic shift di . - Compression: divisione ( primo, lontano da potenze di ); MAD , primo.
- Separate chaining: bucket = lista; .
get/put/remove: caso pessimo (tutte nello stesso bucket); caso medio sotto uniform hashing; in pratica . - Rehashing se supera la soglia (es. ): nuovo array , nuova , reinserimento di tutte le entry in ; ammortizzato.
- Esempio: , , chiavi , , , , ; .
- Pro/contro: semplice, veloce in media, nessun ordine richiesto / caso pessimo lineare, dipende da , spazio per basso.
- Errori: al caso pessimo; ; copiare invece di reinserire dopo il rehashing; somma dei caratteri come hash di stringa.