Tabelle hash
Si è sempre assunto che i nodi fossero numerati da 1 a N. Nel mondo reale, però, possono assumere anche stringhe, come ad esempio nomi di città.
Serve quindi un dizionario che implementa una relazione univoca tra gli elementi del domino D (chiavi) e quelli del codominio C (valori).
Primitive
new_dictionary(): restituisce un dizionario vuotolookup(D, k): restituisce il valore associato in D a k se esiste,nilaltrimentiinsert(D, k, v): inserisce l’associazionek, vremove(D, k): rimuove l’associazione chiave-valore.
Per salvare queste coppie si possono utilizzare varie strutture dati, tra cui array, liste ecc, ma sarebbero tutte un po’ costose rispetto alle tabelle hash.
Tabelle hash
Le coppie (k, v) sono memorizzate in un array, e la posizione della coppia dipende da k, che è processata da una funzione hash.
Le tabelle ad accesso diretto funzionano solo se l’insieme universo è relativamente piccolo. Se le chiavi usate sono poche, c’è un grande spreco di memoria, perché la tabella è grande quanto l’insieme universo.
Una funzione hash si dice perfetta se . Questo tipo di funzione è piuttosto complicata da implementare.
Problema
Può capitare che , eppure . Come si possono salvare entrambe le chiavi? Una funzione hash si dice uniforme, se distribuisce uniformemente le chiavi di U in M.
è la probabilità che k sia inserita nella tabella. è la probabilità che una chiave venga inserita nella cella i della tabella. .
Si vuole raggiungere
Le collisioni non vanno eliminate, ma si cerca di ridurle al minimo. Se comunque si verificano collisioni, si devono prevedere posizioni alternative, stabilire come cercare le chiavi in queste nuove posizioni e tenere bassi i costi.
Esistono due alternative:
- liste di trabocco
- indirizzamento aperto
Liste di trabocco
Si usa una lista per memorizzare tutte le chiavi che finiscono nella stessa cella, e le nuove chiavi vengono inserite in testa a H[h(k)].
Il caso peggiore è quello in cui tutte le chiavi sono in un’unica lista. Questo ovviamente ha il costo delle liste, che è per l’insert di un nuovo elemento in testa, e per il lookup e la remove, con n numero di chiavi memorizzate.
Il fattore di carico se è > 1, allora nella tabella c’è sicuramente una collisione. n: numero chiavi m: capacità tabella hash
Se h è una funzione uniforme semplice, allora la lunghezza attesa delle liste di trabocco è .
Per una funzione uniforme semplice, in media la ricerca della chiave per lookout e remove, costa se è senza successo, mentre con successo.
Se , allora , e quindi i costi sopra diventano .
Indirizzamento aperto
Non si utilizzano le liste per salvare le collisioni, ma piuttosto si cerca un’altra cella, partendo dall’indice h(k) e cercando anche in tutte le posizioni alternative.
h(k, 0), h(k, 1), h(k, 2), ..., h(k, m-1)
con .
Quando si cancella un elemento, si inserisce un valore speciale DEL, per capire che in quella cella c’era effettivamente un elemento, e di continuare la ricerca se ancora non si è trovato l’elemento cercato.
Hashing uniforme Ogni chiave ha la stessa probabilità di avere come sequenza di ispezione una qualunque delle permutazioni di . Questo concetto è però di difficile implementazione.
Ispezione lineare
, con h(k, i) è l’ispezione i-esima per la chiave k, h'(k) è la funzione hash da applicare a k, c è la costante e i è il numero di ispezione.
Questa ispezione lineare tende a creare agglomerati.
- Il fattore di carico, però, è tra 0 e 1, .
- La tabella di può riempire e andare in overflow
Per evitare l’overflow, quando il fattore di carico arriva a 0.5-0.75, si alloca una nuova tabella di dimensione 2m, e si spostano tutti i dati. Questa nuova tabella non ha DEL, viene dimezzato e il costo per farlo è di .