Hashing

Transcript

Hashing
Laboratorio di Algoritmi e Strutture Dati
II Semestre 2005/2006
Hash Tables
Marco Antoniotti
Suggerimenti dalla regia sul tema
“ottimizzazione”
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
1/26
1
Hashing
•
•
•
Sminuzzare, tagliuzzare
Idea principale: memorizzare i valori in una tabella indicizzata per
chiavi; l’indice è il risultato di un’operazione di codifica della chiave
Hash function: una funzione h : K → N
che associa un indice della tabella (previo modulo) ad una chiave
•
Problema: collisioni
•
Bilancio spazio-tempo dati N elementi e M caselle in tabella
€
– Se la M > N non ci sono problemi
– Se non ci sono limiti di tempo basta introdurre una ricerca sequenziale per
risolvere il problema delle collisioni
– Con limitazioni in spazio e tempo allora si usano schemi di hashing
sofisticati
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
2/26
Come scegliere una buona funzione di
hashing
• Obiettivo: “mischiare” una chiave
– Efficienza computazionale
– Ogni posizione in tabella dovrebbe risultare ugualmente probabile
per ogni chiave
• Problema ampiamente studiato
• Esempio: numeri telefonici
– Bbuono: le ultime tre cifre
– No bbuono: il “prefisso”
• Esempio: data di nascita
– Bbuono: il giorno di nascita
– No bbuono: l’anno di nascita
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
3/26
2
Hash Function: esempio per stringhe
•
Una funzione di hash per le stringhe potrebbe essere la seguente
#include <string>
int hashCode(std::string s) {
int hash = 0;
for (int i = 0; i < s.length(); i++)
hash = (hash * 31) + s[i]; // Oppure s.at(i);
return hash;
}
•
Equivalente a h = 31L-1s0 + 31L-2s1 + … + 31sL-2 + sL-1
– Metodo di Horner: O(L) per una stringa di lunghezza L
– Attenzione: il valore di h di hashCode va preso in modulo lunghezza della
tabella M
– Usare - in questo caso - (h & 0x7fffffff) % M è meglio
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
4/26
Hash Function: esempio per stringhe
•
Una funzione di hash per le stringhe in C
int hashCode(char * s) {
int hash = 0; char * p = s;
for (; p; p++)
hash = (hash * 31) + (*p);
return hash;
}
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
5/26
3
Codice di hash
•
Proprietà: per ogni chiave x ed y:
– Chiamate ripetute hashCode(x) devono restituire valori uguali data una
implementazione di operator== sull’insieme K
– Inoltre, se vale x == y allora dobbiamo avere
hashCode(x) == hashCode(y)
•
Implementazione di default
– Basata sull’indirizzo in memoria dell’oggetto e/o della classe, oppure sulla
rappresentazione binaria dell’oggetto
•
Implementazioni speciali
– La STL contiene le classi “Containers” hash_map, hash_multimap, e
hash_multiset che richiedono di fornire esplicitamente le funzioni di
uguaglianza e di hash (questa funzione è tecnicamente un oggetto
“funzionale” nel gergo STL C++)
– La libreria Java fornisce i metodi hashCode ed equals a livello di
java.lang.Object: implementazioni speciali di hashCode si hanno, per
esempio, per String, URL, Date etc etc
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
6/26
Collisioni
•
Collisione = due chiavi hanno lo stesso codice di hash
–
–
–
Essenzialmente inevitabile
Il problema del compleanno: quante persone devono in media entrare in una stanza
prima che due abbiano lo stesso compleanno? 23
Con M valori di hash, ci si può attendere una collisione dopo
√(1/2 M !) inserimenti
•
Conclusione: non si possono evitare
collisioni a meno di avere una quantità
di memoria gigantesca
•
Sfida: gestire le collisioni in
modo efficiente
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
7/26
4
Due soluzioni al problema delle
collisioni
•
“Separate chaining”
–
–
–
–
•
M << N
≈ N/M chiavi per posizione
I valori sono inseriti in liste
corrispondenti ad ognuna delle
caselle
Bisogna cercare (linearmente) i
valori nelle liste
“Open addressing”
–
–
–
–
M >> N
Abbondanza di caselle vuote in
tabella
Quando si ha una collisione si cerca
una nuova casella vuota in memoria
Le analisi delle “tracce” delle
operazioni di gestione collisioni
sono molto complesse
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
8/26
Separate chaining in C++
•
•
Nel seguito verrà presentata una implementazione delle Hash Tables
basata su quella presentata in Stroustroup
Separate chaining
–
–
–
–
Array di M liste (catene o “chains”)
Hash: mappa una chiave su [0..M-1]; chiamiamo queto valore h
Insert: inserisce il valore all’inizio dell’h-esima lista
Ricerca: cerca solo la lista h-esima
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
9/26
5
Separate Chaining
• Un codice semplificato potrebbe essere il seguente
template <class Key, class Value,
class H = Hash<Key>,
// Oggetto funzione
class EQ = equal_to<Key>, … >
// Oggetto funzione
class hash_table {
// …
private: // Rappresentazione
struct Entry {
Key key;
Value val;
bool erased;
Entry* next;
Entry(Key k, Value v, Entry* n)
: key(k), val(v), erased(false), next(n) {}
};
vector<Entry> v;
vector<Entry*> b;
};
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
10/26
Separate Chaining
• Un po’ di definizioni extra
template <class Key, class Value,
class H = Hash<Key>,
// Oggetto funzione
class EQ = equal_to<Key>, … > // Oggetto funzione
class hash_table {
// …
private: // Rappresentazione
float max_load;
float grow;
size_t no_of_erased;
H hash;
// La funzione di hash.
EQ eq;
// La funzione di uguaglianza.
const Value default_value;
};
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
11/26
6
Separate Chaining
• L’operazione search diventa (dichiarazioni)
template <class Key, class Value,
class H = Hash<Key>,
// Oggetto funzione
class EQ = equal_to<Key>, … > // Oggetto funzione
class hash_table {
// …
Value& operator[](const Key&);
// L’operatore [] e` la
// nostra search.
iterator find(const Key&);
const_iterator find(const Key&) const;
};
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
12/26
Separate Chaining
•
L’operazione search diventa (definizione)
template <class Key, class Value,
class H = Hash<Key>,
// Functor
class EQ = equal_to<Key>, … >
// Functor
hash_table<Key, Value, H, EQ, …>::Value&
// Notare la referenza!
hash_table<Key, Value, H, EQ, …>::operator[](const Key& k) {
size_t i = hash(k) % b.size();
for (Entry* p = b[i]; p; p = p->next) {
if (eq(k, p->key)) {
// Trovato.
if (p->erased) {
// Re-inserzione.
p->erased = false;
no_of_erased--;
p->value = default_value;
}
return p->value;
}
// Non trovato.
// “grow” and “resize” if needed.
v.push_back(Entry(k, default_value, b[i])); // Notare l’inizializzazione
// di ‘next’.
b[i] = &v.back();
return b[i]->value;
}
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
13/26
7
Hashing
• Una definizione completa della funzione di hash che ci permette
di lavorare in maniera coordinata con le definizioni precedenti
può essere
template <class T>
struct Hash : unary_function<T, size_t> {
size_t operator() (const T& key) const;
}
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
14/26
Hashing
• Scegliere una buona funzione di hash è quasi un’arte. Di solito,
in C++, fare lo XOR dei bit che rappresentano l’oggetto è
abbastanza ragionevole
template <class T>
size_t Hash<T>::operator() (const T& key) const {
size_t res = 0;
size_t len = sizeof(T);
const char* p = reinterpret_cast<const char*>(&key);
// Considera i bytes che compongono l’oggetto.
while (len--) res = (res << 1)^*p++;
return res;
}
• Come abbiamo visto altre funzioni per tipi ben precisi sono
possibili
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
15/26
8
Prestazioni del “separate hashing”
• Il costo di una ricerca e proporzionale alla lunghezza della lista
(catena) da esplorare
• Computo banale: lunghezza media N/M
• Caso peggiore: tutte le chiavi finiscono nella stessa casella
• Teorema: sia α = N/M > 1 la lunghezza media di una catena;
per ogni t > 1, la probabilità che la lunghezza della lista sia > t α
è esponenzialmente piccola in t.
Dipende da quanto la funzione di hash sia casuale
• Parametri
– Se M è troppo grande ⇒ troppe catene vuote
– Se M è troppo piccolo ⇒ le catene diventano troppo lunghe
– Scelta tipica α = N/M ≈ 10 ⇒ tempo costante per search/insert
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
16/26
Open Addressing / Linear Probing
•
Open Addressing / Linear Probing: array di dimensione M
– Tipicamente M ≈ 2 N
– Hash: si computa il valore di hash h tra 0 e M-1
– Insert: se la casella h è vuota si inserisce il valore lì; altrimenti si prova h+1,
h+2, h+3 e così via…
– Search: se la casella h è piena e contiene la chiave ricercata allora si
ritorna il valore associato, altrimenti si ripete per h+1, h+2, h+3 e così via
fino ad una casella vuota
•
Problema Cluster:
– Blocco contiguo
di elementi
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
17/26
9
Open Addressing / Linear Probing
• Un codice semplificato potrebbe essere il seguente
template <class Key, class Value,
class H = Hash<Key>,
class EQ = equal_to<Key>, … >
class hash_table {
// …
private: // Rappresentazione
// Functor
// Functor
vector<Key*> keys;
vector<Value> values;
};
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
18/26
Open Addressing / Linear Probing
• L’operazione search diventa (dichiarazioni)
template <class Key, class Value,
class H = Hash<Key>,
// Functor
class EQ = equal_to<Key>, … >
// Functor
class hash_table {
// …
Value& operator[](const Key&);
// L’operatore [] e` la
// nostra search.
};
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
19/26
10
Open Addressing / Linear Probing
•
L’operazione search diventa (definizione)
template <class Key, class Value,
class H = Hash<Key>,
// Functor
class EQ = equal_to<Key>, … >
// Functor
hash_table<Key, Value, H, EQ, …>::Value&
// Notare la referenza!
hash_table<Key, Value, H, EQ, …>::operator[](const Key& k) {
int M = keys.size();
size_t i = hash(k) % keys.size();
for (Key* p = keys[i]; p != 0; i = (i + 1) % M) {
if (eq(k, *p)) { // Trovato.
break;
return values[i];
}
•
NB: in questo caso non stiamo considerando il valore di default
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
20/26
Prestazioni del “linear probing”
•
•
La complessità di insert e search dipendono dalle dimensioni del
cluster
Computo banale: lunghezza media di un cluster α = N/M
•
Caso peggiore: tutte le chiavi finiscono nello stesso cluster
•
Teorema [Knuth 1962]: sia α = N/M < 1 la lunghezza media di un
Dipende da quanto la funzione di hash sia casuale
cluster; allora si ha
– Ma la probabilità di avere tutte le chiavi in un cluster cresce
– Insert:
– Search:
•
1
1 
1+

2  (1− α )2 
1
1 
1+

2  (1− α ) 
€
Parametri
– Se M è troppo grande ⇒ troppe catene vuote
– Se M€è troppo piccolo ⇒ le catene diventano troppo lunghe
– Scelta tipica α = N/M ≈ 10 ⇒ tempo costante per search/insert
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
21/26
11
Doppio hashing
•
Per evitare di creare clusters troppo grandi si può usare una seconda
funzione di hashing per calcolare il “prossimo” indice da controllare
•
•
Hash: mappa una chiave in un indice [0..M-1]
Seconda hash: mappa una chiave ad un valore di “salto” k diverso da
zero
•
Esempio: k = 1 + (hashCode(key) % 97)
•
•
Risultato: il “salto” genera diverse linee di ricerca in caso di collisioni
Pratica: se si fa si che M e k siano relativamente primi la prestazione è
migliore
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
22/26
Prestazioni del “doppio hashing”
•
•
La complessità di insert e search dipendono dalle dimensioni del
cluster
Computo banale: lunghezza media di un cluster α = N/M
•
Caso peggiore: tutte le chiavi finiscono nello stesso cluster
•
Teorema [Guibas-Szemeredi]: sia α = N/M < 1 la lunghezza media di
un cluster; allora si ha
Dipende da quanto la funzione di hash sia casuale
– Ma la probabilità di avere tutte le chiavi in un cluster cresce
– Insert:
1
1− α
– Search:
•
Parametri
€
1
ln(1+ α )
α
– Se M è troppo grande ⇒ troppe caselle vuote
€
– Se M è troppo piccolo ⇒ i clusters si fondono
– Scelta tipica M ≈ 2N ⇒ tempo costante per search/insert
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
23/26
12
Costi/benefici per hashing
• Separate chaining rispetto a linear probing/doppio hashing
– Costo di puntatori rispetto a caselle vuote nella tabella
– Tabella piccola + puntatori rispetto ad una tabella grande ma
coerente
• Linear probing rispetto a double hashing
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
24/26
Attacchi basati sulla complessità
algoritmica
• Quanto è importante in pratica l’assunzione che la funzione di
hash sia “casuale”?
– Situazioni ovvie: impianti nucleari, controllo traffico aereo
– Situazioni sorprendenti: attacchi “denial of service” (DOS)
Un cracker malvagio scopre i dettagli della vostra
funzione di hash ‘ad hoc’ (ad esempio leggendo il sorgente di Java 1.1)
e costruisce un attacco tale da riempire una casella sola della vostra
hash table; in tal modo le prestazioni del vostro sistema decadono
rapidamente
• Casi reali [Crosby-Wallach 2003]
– Bro server: dei pacchetti accuratamente selezionati sono mandati
al server usando meno banda di una connessione dial-up
– Perl 5.8.0: si inseriscono delle stringhe scelte artatamente in un
array associativo
– Kernel Linux 2.4.20: si salvano files con nomi particolari
• Riferimento: http://www.cs.rice.edu/~scrosby/hash
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
25/26
13
Attacchi basati sulla complessità
algoritmica
•
•
•
Quanto sono semplici questi attacchi?
Ad esempio: è possibile rompere Java con chiavi come stringhe?
–
hashCode è parte di Java 1.5
–
–
hashCode di “BB” è uguale a “Aa”
Ora possiamo creare 2N stringhe che hanno tutte lo stesso hashCode
Aggiustamenti
–
–
Funzioni di hash crittograficamente sicure
Funzioni di hash universali
II Semestre 2005/2006
Laboratorio Algoritmi - Marco Antoniotti
26/26
14