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