Hash Tables Indirizzamento diretto Tabelle Hash Risoluzioni di collisioni Indirizzamento aperto
Indirizzamento diretto K (chiavi effettive) U (Insieme di tutte le chiavi) key dati aggiuntivi Lindirizzamento diretto viene utilizzato quando linsieme delle chiavi U è piccolo T[0,…,8]
Indirizzamento diretto Lindirizzamento diretto viene rappresentato, generalmente, da un array, dove ogni casella corrisponde ad una chiave (key) dellinsieme U. Ogni casella dellarray deve corrispondere ad un elemento distinto e unico dellinsieme delle chiavi U. La casella T[key] punta allelemento con la chiave key e le sue informazioni aggiuntive. Se T[key]=NIL, allora non esiste nessun elemento da mappare con chiave key. Se |U| non è grande, larray non raggiunge grosse dimensioni, utilizzando efficacemente lindirizzamento diretto.
Indirizzamento diretto Operazioni: DIRECT-ADDRESS-SEARCH(T,key) 1. return T[key] DIRECT-ADDRESS-INSERT(T,key) 1. T[key] key DIRECT-ADDRESS-DELETE(T,key) 1. T[key] NIL Queste tre operazioni richiedono tempo O(1).
Hash Tables Lindirizzamento diretto diventa difficoltoso quando U risulta essere molto grande. Inoltre, se |K| << |U|, ci sarebbe un notevole spreco di spazio di memoria. Esempio: Prendiamo un dizionario della lingua italiana di circa parole con lunghezza massima di 15 lettere. Se U corrisponde allinsieme di tutte le stringhe di lettere L={a, b, c,…, z} di lunghezza massima di 15, sia ha: se |L|=21 Parole presenti nel dizionario
Hash Tables key 1 K (chiavi effettive) U (Insieme di tutte le chiavi) --- T[0, 1, …, m-1] key 2 key 3 key 0 h(key 0 )= h(key 1 ) h(key 2 ) h(key 3 ) Viene definita una funzione hash che calcola la casella da associare a ciascuna chiave (key i ). h: U {0, 1, 2, …, m-1} Si dice, che h(key i ) è il valore hash della chiave (key i ).
Hash Tables Tramite le Hash Tables, la quantità di memoria necessaria può essere ridotta a Θ(|K|), mantenendo un costo della ricerca di O(1) nel caso medio. Cè la possibile presenza di collisioni, ossia quando due chiavi distinte hanno lo stesso valore hash. key ikey j e h(key i )=h(key j ) Se la funzione hash distribuisse in modo uniforme le chiavi nelle differenti celle, la probabilità di collisioni sarebbe minima. Essendo |U|>m (con m la dimensione di T), evitare del tutto le collisioni è impossibile.
Gestire le collisioni Una possibile soluzione è quella di inserire tutti gli elementi con lo stesso valore hash in una lista concatenata. key 1 K (chiavi effettive) U (Insieme di tutte le chiavi) --- T[0, 1, …, m-1] key 2 key 3 key 0 key 1 key 2 key
Gestire le collisioni Operazioni: CHAINED-HASH-INSERT(T,x) 1. inserisci lelemento x allinizio della lista T[h(key)] CHAINED-HASH-SEARCH(T,key) 1. ricerca un elemento con chiave key nella lista T[h(key)] CHAINED-HASH-DELETE(T,x) 1. elimina lelemento x nella lista T[h(key)] La cella T[h(key)] contiene un puntatore allinizio della lista concatenata in cui vengono inseriti tutti i valori con lo stesso valore hash. Se nessun elemento è stato ancora inserito, T[h(key)] contiene NIL.
Gestire le collisioni Linserimento richiede un tempo O(1). La ricerca richiede un tempo proporzionale alla lunghezza della lista nel caso peggiore. Per quanto riguarda la rimozione, può essere compiuta in tempo O(1) quando si usa una lista bidirezionale (doubly linked list). Se la lista concatenata è unidirezionale, per poter eseguire la rimozione di un elemento bisogna fare una ricerca per conoscere lelemento che precede x. Il tempo quindi è proporzionale alla lunghezza della lista nel caso peggiore.
Analisi di hashing con liste concatenate Nel caso peggiore tutti gli elementi hanno lo stesso valore di hash e quindi tutto viene gestito come se fosse una lista concatenata. Nella nostra analisi si fa lassunzione di uniformità semplice della funzione hash. Questo vuol dire che la funzione hash distribuisce bene linsieme delle chiavi da memorizzare sulle m celle. Si assume anche che il calcolo della funzione hash h(key) è costante,O(1). Si assume, inoltre, che laccesso alle celle si fatto in tempo costante, O(1). Si definisce il fattore di carico α per T come n/m, cioè il numero medio di elementi in ogni lista quando ne sono stati inseriti n in totale.
Analisi di hashing con liste concatenate a.Le chiavi vengono equamente distribuite per tutte le m celle. Dunque, la lunghezza media delle liste equivale a α. b.Per cui il numero medio di elementi da esaminare in una ricerca senza successo è pari a α. c.A questo si aggiunge il tempo di calcolo di h(key), ottenendo un tempo medio di Θ(1+ α). Sotto queste ipotesi, in una tabella hash che risolve le collisioni per concatenazione una ricerca senza successo richiede un tempo medio di Θ(1+ α).
Analisi di hashing con liste concatenate Sotto queste ipotesi, in una tabella hash che risolve le collisioni per concatenazione una ricerca con successo richiede un tempo medio di Θ(1+ α). a.Le chiavi vengono equamente distribuite per tutte le m celle. b.Lanalisi è più complessa, in quanto dipende in quale punto della lista mediamente si trova lelemento x da trovare. c.Per eseguire questo calcolo più agilmente si suppone che gli elementi vengono inseriti alla fine della lista. Questo non cambia il risultato.
Analisi di hashing con liste concatenate d.Sotto questa ipotesi, quando lelemento i-esimo è inserito, esso viene messo in una lista lunga in media (i-1)/m. e.Per trovare lelemento i-esimo, bisogna calcolare h(key i ) ed esaminare mediamente (i-1)/m elementi. f.Dunque il tempo medio complessivo è:
Analisi di hashing con liste concatenate Il significato di questa analisi? Se gli elementi allinterno della tabella hash sono n e risultano proporzionali a m, si ha n=O(m) e, quindi, α = O(m)/m = O(1). Quindi, la ricerca media richiede in media tempo costante. Essendo la ricerca loperazione più costosa, si ottiene un tempo medio per tutte le operazioni pari a O(1).
Requisiti per una buona funzione hash Lassunzione di uniformità semplice della funzione hash equivale a supporre che ogni chiave ha uguale probabilità di essere collocata in una qualsiasi cella. Più formalmente, supponiamo che ogni chiave key è pescata indipendentemente dalle altre da U con probabilità P(key). Allora si ha: Raramente, si è a conoscenza della distribuzione P. Per questo motivo si utilizzano in pratica delle euristiche che possono produrre una buona funzione hash.
Il metodo della divisione Nel metodo della divisone si utilizza il resto di key diviso m: h(key) = key mod m La scelta di m deve essere fatto in maniera opportuna, che dipende, in genere, dalla tipologia di chiavi (numeri decimali, i byte di caratteri ASCII,…). Una buona scelta è quella di scegliere un numero primo non vicino a una potenza di 2. Per esempio, se n= 2000 e le chiavi sono equamente probabili, il numero primo 701, che è vicino a 2000/3, può essere una buona scelta. Si ha quindi: h(key) = key mod 701
Il metodo della moltiplicazione Richiede due passi: 1.Si moltiplica K per una costante A, 0 < A < 1, e si estrae la parte non intera. 2.Si moltiplica il risultato per m e si prende la parte intera. In breve: La scelta di m non è più critica. Quindi, si può sceglie un valore che renda la moltiplicazione efficiente. Anche se la funzione hash è buona per ogni valore di A, ci sono alcune scelte migliori. Per esempio: A(5 – 1)/2= …
Indirizzamento aperto Nellindirizzamento aperto ogni cella della tabella contiene o un elemento dellinsieme U o NIL. Quando si ricerca un elemento, si esaminano sistematicamente le posizioni della tabella finché o si trova lelemento desiderato o è chiaro che non è presente nella tabella. Non ci sono puntatori e liste concatenate, tutto è memorizzato nella tabella. Quindi, la tabella hash può riempirsi finché nessun altro elemento può essere inserito. Il fattore di carico α 1.
Scansione nellindirizzamento aperto Quando si ricerca o si inserisce un elemento, si esaminano in sequenza le posizioni della tabella finché o si trova lelemento desiderato o si trova NIL. La sequenza è una permutazione degli indici della tabella e dipende dalla chiave key scelta. Quindi la funzione hash è del tipo: h: U x {0,1, 2, …, m-1} {0,1, 2, …, m-1} E la sequenza di scansione è: Dunque, si ha un problema di quante posizioni della tabella in media si vengono visitate.
Inserimento nellindirizzamento aperto HASH-INSERT(T,key) 1. i 0 2. repeat j h(key,i) 3. if T[j] = NIL 4. then T[j] key 5. return j 6. else i i+1 7. until i = m 8. error overflow sulla tabella hash La ricerca della posizione termina quando si trova una posizione vuota.
Ricerca nellindirizzamento aperto HASH-SEARCH(T,key) 1. i 0 2. repeat j h(key,i) 3. if T[j] = key 4. then return j 5. else i i+1 6. until T[j] = NIL o i = m 7. return NIL La ricerca della chiave termina quando si trova la chiave o una posizione vuota, poiché key sarebbe stata inserita proprio li.
Ipotesi di uniformità della funzione hash Nellipotesi di uniformità della funzione hash si considera equamente probabile una qualunque delle m! permutazioni delle posizioni data la chiave key. Questa ipotesi generalizza quella di uniformità semplice, in quanto viene restituita dalla funzione hash non un solo valore di {0, 1, 2, …, m-1}, ma unintera sequenza. Per questo motivo, risulta difficile generare una buona funzione hash. Ci sono alcune approssimazioni: la scansione lineare, quella quadratica e il doppio hash.
Scansione lineare Data una funzione hash h: U {0, 1, …, m-1}, il metodo di scansione lineare usa la funzione hash: h(key,i) = (h(key) + i) mod m La scansione lineare è facile da realizzare. Presenta un problema conosciuto come fenomeno di agglomerazione primaria. Le posizioni occupate si accumulano in lunghi tratti, aumentando il tempio medio di ricerca.
Scansione quadratica Data una funzione hash h: U {0, 1, …, m-1}, il metodo di scansione quadratica usa la funzione hash h(key,i) = (h(key) + c 1 i + c 2 i 2 ) mod m Le scansioni successive dipendono in modo quadratico dal numero i di accessi. Questo metodo funziona meglio della scansione lineare, ma i valori c 1, c 2 e m devono essere scelti in modo che la sequenza scansioni lintera tabella. Come per la scansione lineare, laccesso iniziale determina lintera sequenza, quindi sono usate solo m distinte sequenze di scansione. Per questo si dice che cè un problema di agglomerazione secondaria.
Hashing doppio Il metodo di hashing doppio usa una funzione hash del tipo: h(key,i) = (h 1 (key) + i h 2 (key)) mod m Diversamente dalla scansione lineare o quadratica, la sequenza di scansione dipende dalla posizione iniziale e dalla distanza determinata dalla seconda funzione di hash. Per esempio, si può usare: h 1 = key mod m h 2 = 1+ (key mod m) Questo permette di usare Θ(m 2 ) sequenze di scansione, avvicinandosi allassunzione di uniformità della funzione di hash.
Analisi dellindirizzamento aperto Diamo solo i risultati: Data una tabella hash a indirizzamento aperto con fattore di carico α=n/m <1, il numero medio di accessi di una ricerca senza successo è al più 1/(1-α), assumendo luniformità della funzione hash. Data una tabella hash a indirizzamento aperto con fattore di carico α=n/m <1, il numero medio di accessi di una ricerca con successo è al più 1/α ln(1/(1-α)), assumendo luniformità della funzione hash e che ogni chiave sia ricercata nella tabella in modo equamente probabile.