Vai al contenuto

Algoritmi/Le tabelle di hash

Wikibooks, manuali e libri di testo liberi.

tion

Indice del libro

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

[modifica | modifica sorgente]

Metodo moltiplicativo (chiavi in virgola mobile)

[modifica | modifica sorgente]

Per una chiave compresa nell'intervallo :

  1. 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;
  2. si moltiplica per il numero per trovare un valore compreso nell'intervallo tra 0 e ;
  3. si prende l'intero superiore, poiché è un numero intero.

Metodo modulare (chiavi intere)

[modifica | modifica sorgente]

Si 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)

[modifica | modifica sorgente]

Data una costante (per esempio: ):

Tabelle di hash per stringhe

[modifica | modifica sorgente]

Metodo modulare per stringhe corte

[modifica | modifica sorgente]

Pensando 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

[modifica | modifica sorgente]

La 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 | modifica sorgente]

È 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

[modifica | modifica sorgente]

Ogni 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

[modifica | modifica sorgente]

Ogni 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 | modifica sorgente]
  • 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

[modifica | modifica sorgente]

L'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: