Algoritmi/Le tabelle di hash
tion
La tabella di hash è un tipo di dato astratto che utilizza uno spazio di memoria congruente con il numero di dati, in cui si garantiscono operazioni prossime al costo unitario, ovvero con un tempo medio di accesso in ricerche/cancellazioni/inserzioni di tipo unitario.
- tabelle ad accesso diretto: la chiave è l'indice del vettore → complessità lineare, ma: non sono utilizzabili quando il numero delle chiavi è troppo grande e non si può allocare in memoria;
- strutture ad albero: complessità logaritmica, ma si spera che l'albero non degeneri;
- tabelle di hash: c'è una funzione intermedia detta funzione di hash che manipola la chiave e la trasforma in un indice.
Il numero delle chiavi contenute in una tabella di hash di dimensione è molto minore della cardinalità dell'universo delle chiavi: .
Se ogni chiave nell'universo ha un indirizzo compreso tra 0 e , la funzione di hash associa alla chiave l'indice della tabella .
La tabella di hash deve garantire una buona distribuzione: se le chiavi sono equiprobabili allora i valori devono essere equiprobabili, cioè la distribuzione deve essere semplice uniforme e non si deve polarizzare su determinate chiavi.
Tabelle di hash per valori numerici
modificaMetodo moltiplicativo (chiavi in virgola mobile)
modificaPer una chiave compresa nell'intervallo :
- normalizzazione: si divide (offset tra la chiave e l'estremo inferiore dell'intervallo) e (ampiezza dell'intervallo) → si ottiene un valore compreso tra 0 e 1;
- si moltiplica per il numero per trovare un valore compreso nell'intervallo tra 0 e ;
- si prende l'intero superiore, poiché è un numero intero.
Metodo modulare (chiavi intere)
modificaSi applica la funzione mod()
a una chiave intera:
Per limitare le collisioni, si prende per un numero primo. Esempi antitetici di tabelle di hash sono la funzione : concentra tutti i numeri in valori 0 e 1 → tantissime collisioni perché si limita ai primi n bit più significativi.
Metodo moltiplicativo-modulare (chiavi intere)
modificaData una costante (per esempio: ):
Tabelle di hash per stringhe
modificaMetodo modulare per stringhe corte
modificaPensando ogni carattere come numero, la stringa è la rappresentazione compatta di un polinomio. Valutando il polinomio in un punto (di solito ), si ottiene il valore numerico intero → si applica il metodo modulare per chiavi intere.
Le operazioni per valutare il polinomio non sono però efficienti → questo metodo si può usare per stringhe corte.
Metodo modulare per stringhe lunghe
modificaLa valutazione del polinomio è effettuata tramite il metodo di Horner, che considera ricorsivamente polinomi di grado 1:
La base x del polinomio può essere un numero primo o un numero pseudocasuale a = (a * b) mod (M - 1)
(hash universale). Per calcolare la chiave k, per ogni polinomio di primo grado valutato si deve fare a ogni passo la funzione mod M
.
Gestione delle collisioni
modificaÈ inevitabile il fenomeno della collisione, ovvero quando chiavi diverse corrispondono allo stesso valore di indice; si può ridurre con buone funzioni di hash.
Si ha una collisione se:
Linear chaining
modificaOgni elemento della tabella contiene un puntatore alla testa di una lista concatenata che contiene tutte le chiavi collidenti.
- Operazioni sulla lista
- inserzione: non si hanno esigenze di ordinamento → l'inserzione meno costosa è quella in testa: ;
- ricerca: se = numero delle chiavi, = dimensione della tabella di hash:
- caso peggiore: tutte le chiavi si concentrano in una sola lista → operazione lineare nella dimensione della lista, ma: caso poco frequente: ;
- caso medio: le chiavi sono tutte uniformemente distribuite → la dimensione media delle liste è uguale al fattore di carico : (un buon valore di è 0,1);
- cancellazione:
- se la lista è doppio-linkata: ;
- altrimenti, il costo è legato alla ricerca dell'elemento.
Open addressing
modificaOgni cella può contenere al massimo un solo elemento ( ), e in caso di collisione il valore da inserire va inserito in un'altra cella, verificando che sia vuota (probing = ricerca di una cella vuota). Le celle rimanenti vengono sondate nell'ordine di una delle permutazioni stabilita dalla funzione di hash, in funzione anche del tentativo : .
Linear probing
modifica- inserzione: si sonda la cella di indice successivo e si inserisce nella prima casella vuota;
- ricerca: si passa di volta in volta alla cella di indice successivo, ricordando che al termine della tabella si deve passare alla prima cella (
mod M
a ogni incremento). È una gestione implicita delle liste all'interno di un vettore senza ricorrere al concetto di puntatore; - cancellazione: bisogna distinguere le celle vuote dalle celle svuotate dalla cancellazione, perché altrimenti la ricerca successiva si fermerebbe alla cella svuotata.
Sperimentalmente, tentativi in media di probing per la ricerca:
- search miss:
- search hit:
Se → search miss ~1 → efficiente.
Double hashing
modificaL'indice della cella successiva in caso di probing con insuccesso viene calcolato da un'altra funzione di hash h2.
Sperimentalmente, tentativi in media di probing per la ricerca:
- search miss:
- search hit: