Così come per le liste, un dizionario è costituito da un insieme di coppie di elementi (k,e) chiaveelemento.

Documenti analoghi
Il TDA Dictionary. Definizione informale. I metodi del TDA Dictionary 1. Applicazioni. I metodi del TDA Dictionary 2. I metodi del TDA Dictionary 3

Sommario. Tabelle ad indirizzamento diretto e hash Funzioni Hash

Esercizi Capitolo 7 - Hash

Algoritmi e Strutture Dati

Corso di Informatica Generale (C. L. Economia e Commercio) Ing. Valerio Lacagnina Rappresentazione dei numeri relativi

Linguaggio C - sezione dichiarativa: costanti e variabili

PROBLEMI ALGORITMI E PROGRAMMAZIONE

Equazioni lineari con due o più incognite

Strutture dati e loro organizzazione. Gabriella Trucco

modificato da andynaz Cambiamenti di base Tecniche Informatiche di Base

Programmazione in Java (I modulo)

Sviluppo di programmi

Addizionatori: metodo Carry-Lookahead. Costruzione di circuiti combinatori. Standard IEEE754

La codifica. dell informazione

ADT Dizionario. Come nella Mappa: Diversamente dalla Mappa:

LA CODIFICA DELL INFORMAZIONE. Introduzione ai sistemi informatici D. Sciuto, G. Buonanno, L. Mari, McGraw-Hill Cap.2

Somma di numeri binari

Aritmetica dei Calcolatori Elettronici

Programmazione. Cognome... Nome... Matricola... Prova scritta del 22 settembre Negli esercizi proposti si utilizzano le seguenti classi:

L Allocazione Dinamica della Memoria

Rapida Nota sulla Rappresentazione dei Caratteri

Sistemi lineari. Lorenzo Pareschi. Dipartimento di Matematica & Facoltá di Architettura Universitá di Ferrara

La rappresentazione delle informazioni

Rappresentazione dell informazione

INDICI PER FILE. Accesso secondario. Strutture ausiliarie di accesso

Programmazione I Paolo Valente /2017. Lezione 6. Notazione posizionale

Tipi di dati scalari (casting e puntatori) Alessandra Giordani Lunedì 10 maggio 2010

Aritmetica dei Calcolatori

Cap. 2 - Rappresentazione in base 2 dei numeri interi

IL LINGUAGGIO JAVA Input, Tipi Elementari e Istruzione Condizionale

Fondamenti di Informatica T1 Mappe

Teoria dell informazione

Prova d Esame Compito A

Problemi, istanze, soluzioni

Rappresentazione dell Informazione

Calcolo numerico e programmazione Rappresentazione dei numeri

In molte applicazioni sorge il problema di sapere in quanti modi possibili si può presentare un certo fenomeno.

La rappresentazione dei dati

La codifica digitale

R. Cusani, F. Cuomo: Telecomunicazioni - DataLinkLayer: Gestione degli errori, Aprile 2010

Strutture fisiche di accesso

Introduzione alla programmazione Esercizi risolti

Elementi lessicali. Lezione 4. La parole chiave. Elementi lessicali. Elementi lessicali e espressioni logiche. Linguaggi di Programmazione I

Esercizi di Algoritmi e Strutture Dati

Programmazione. Cognome... Nome... Matricola... Prova scritta del 11 luglio 2014

Conversione di base. Conversione decimale binario. Si calcolano i resti delle divisioni per due

La classe java.lang.object

Programmazione ad oggetti

Programmazione Orientata agli Oggetti. Emilio Di Giacomo e Walter Didimo

Lezione 3. I numeri relativi

Il Modello di von Neumann (2) Prevede 3 entità logiche:

Gestione degli impegni Requisiti generali Si fissi come ipotesi che la sequenza di impegni sia ordinata rispetto al tempo,, e che ogni lavoratore abbi

1.2d: La codifica Digitale dei caratteri

Informatica Generale 1 - Esercitazioni Flowgraph, algebra di Boole e calcolo binario

Rappresentazione di numeri interi

Rappresentazione binaria delle variabili (int e char)

Analogico vs. Digitale. LEZIONE II La codifica binaria. Analogico vs digitale. Analogico. Digitale

Caratteri e stringhe

1.2d: La codifica Digitale dei caratteri

Problema: dati i voti di tutti gli studenti di una classe determinare il voto medio della classe.

Utilizza i tipi di dati comuni a tutto il framework.net Accesso nativo ai tipi.net (C# è nato con.net) Concetti fondamentali:

Informatica. Mario Pavone - Dept. Mathematics & Computer Science - University of Catania. Trasferimento. Ambiente esterno.

Rappresentazione e Codifica dell Informazione

Array e Oggetti. Corso di Laurea Ingegneria Informatica Fondamenti di Informatica 1. Dispensa 12. A. Miola Dicembre 2006

ESAME DI FONDAMENTI DI INFORMATICA I ESAME DI ELEMENTI DI INFORMATICA. 28 Gennaio 1999 PROVA SCRITTA

1.2f: Operazioni Binarie

Introduzione alla programmazione Algoritmi e diagrammi di flusso. Sviluppo del software

Codifica binaria. Rappresentazioni medianti basi diverse

Somma di numeri floating point. Algoritmi di moltiplicazione e divisione per numeri interi

Cosa è l Informatica?

Informazione e sua rappresentazione: codifica

Rappresentazione dei Numeri

Informazione e sua rappresentazione: codifica

Modulo 1: Le I.C.T. UD 1.2d: La codifica Digitale dei caratteri

Prova di Laboratorio del [ Corso A-B di Programmazione (A.A. 2004/05) Esempio: Media Modalità di consegna:

Lez. 5 La Programmazione. Prof. Salvatore CUOMO

Descrizione delle operazioni di calcolo. Espressioni costanti semplici

La Rappresentazione dell Informazione

Università degli Studi di Cassino Corso di Fondamenti di Informatica Tipi strutturati: Stringhe. Anno Accademico 2010/2011 Francesco Tortorella

FILE E INDICI Architettura DBMS

Macchine RAM. API a.a. 2013/2014 Gennaio 27, 2014 Flavio Mutti, PhD

Tipi di dato. Il concetto di tipo di dato viene introdotto per raggiungere due obiettivi:

Codifica dei Numeri. Informatica ICA (LC) 12 Novembre 2015 Giacomo Boracchi

RAPPRESENTAZIONE GLI ALGORITMI NOTAZIONE PER LA RAPPRESENTAZIONE DI UN ALGORITMO

Lab 02 Tipi semplici in C

Architetture degli Elaboratori I I Compito di Esonero (A) - 14/11/1996

percorso 4 Estensione on line lezione 2 I fattori della produzione e le forme di mercato La produttività La produzione

Lezione 4. Sommario. L artimetica binaria: I numeri relativi e frazionari. I numeri relativi I numeri frazionari

Aritmetica dei Calcolatori 3

Sistemi di numerazione

La struttura dati CODA

Algoritmi e loro proprietà. Che cos è un algoritmo? Un esempio di algoritmo

19/09/14. Il codice ASCII. Altri codici importanti. Extended ASCII. Tabella del codice ASCII a 7 bit. Prof. Daniele Gorla

Una classe Borsellino. Tipi numerici di base - Costanti. Esempio d uso. Classe Borsellino cont d. Primi passi per l implementazione di Purse

Insiemi Specifiche, rappresentazione e confronto tra realizzazioni alternative.

Corso di Fondamenti di Informatica Il sistema dei tipi in C++

Informatica 1. Prova di recupero 21 Settembre 2001

Argomenti trattati. Informazione Codifica Tipo di un dato Rappresentazione dei numeri Rappresentazione dei caratteri e di altre informazioni

Lezione 9: Puntatori a funzioni. Tipi enumerativi e orientati ai bit

Fondamenti di Informatica - 1. Prof. B.Buttarazzi A.A. 2011/2012

Transcript:

DIZIONARI Così come per le liste, un dizionario è costituito da un insieme di coppie di elementi (k,e) chiaveelemento. In questo testo si utilizzerà spesso il termine elemento, a volte per indicare la coppia (k,e), a volte per indicare l elemento e contenuto nella coppia (k,e). In alcuni casi, per non fare troppa confusione si indicherà come valore dell elemento l elemento e contenuto nella coppia (k,e) chiave-elemento. Il primo utilizzo di un dizionario è di memorizzare degli elementi (k, e) chiave-elemento in modo tale che sia facile in base alle loro chiavi recuperarli in modo rapido ed efficiente. La motivazione per una tale ricerca è che il contenuto dell elemento contenuto nella coppia di elementi è molto più ampio dell informazione riportata nella chiave. Tuttavia la chiave rappresenta il solo mezzo a disposizione per recuperare ogni voce del dizionario. Alle chiavi contenute in un dizionario non è necessariamente richiesto tuttavia di avere una relazione d ordine totale. Infatti per un dizionario, l unica cosa che conta è avere un sistema per dire se due chiavi di due diversi elementi sono uguali oppure no. Invece quando sulle chiavi è definita una relazione d ordine totale, allora si dice che il dizionario è ordinato. Su un dizionario ordinato si possono aggiungere metodi specifici per tale caratteristica aggiuntiva. Un dizionario in termini informatici ( computer dictionary ) è simile ad un dizionario nel senso comune del termine. Tuttavia su questa struttura non è possibile aggiungere o togliere degli elementi, così come dall equivalente struttura informatica che, così come è strutturata, è fatta apposta per continui inserimenti e rimozioni. Già si è visto sul capitolo 9 come possa essere realizzato in maniera efficiente un dizionario utilizzando come struttura dati gli alberi. Si è visto che tali strutture rappresentano delle ottime soluzioni per inserire, ricercare ed eliminare elementi da un dizionario. Infatti il costo di ognuna delle operazioni effettuabili su di un albero di ricerca avanzato (AVL, (2,4) Tree, ecc.) è dell ordine di O(log n). In questo capitolo si affronterà la costruzione di un Dizionario basandosi su strutture del tutto differenti da quelle viste fino ad ora: le hash tables e le skip list. Queste nuove strutture dati permettono in alcuni casi delle prestazioni strabilianti, a costo però di altre preziose risorse come la memoria. 1

Il tipo di dato astratto (ADT) Dizionario Un Dizionario memorizza coppie di valori (k, e) chiave-elemento dove k è la chiave ed e l elemento. Per generalizzare al massimo la struttura dizionario è possibile pensare che sia la chiave, sia l elemento possano essere di qualsiasi tipo e, quindi, in generale di tipo Object. Se si tratta di un dizionario per memorizzare gli studenti di una scuola, probabilmente la chiave di ogni elemento sarà costituita dal numero di matricola dello studente. Potrebbe anche esserci il caso in cui la chiave di ogni coppia chiave elemento coincida con l elemento stesso, cioè che la chiave del dizionario e l elemento da memorizzare coincidano. E per esempio il caso in cui si vogliano memorizzare tutti i numeri primi: il numero primo rappresenta la chiave e coincide con l elemento da memorizzare. Si possono distinguere due casi di dizionari: 1) dizionari non ordinati; 2) dizionari ordinati. In tutti due i casi c è sempre una coppia chiave-elemento in cui la chiave rappresenta un identificatore di che cosa è memorizzato nell elemento in base ad una regola di associazione impostata dal costruttore del dizionario. Così come esiste sempre un modo per determinare se due chiavi sono uguali. Nei dizionari ordinati esiste una relazione d ordine totale fra le chiavi degli elementi ed esistono metodi aggiuntivi dell ADT dizionario che caratterizzano tale tipologia di struttura e che fanno uso di tale relazione d ordine fra le chiavi dei vari elementi. In un dizionario ordinato è possibile determinare l ordine relativo di un elemento rispetto ad un altro dello stesso dizionario in base ad uno strumento comparatore che viene fornito come parametro durante le fasi di costruzione dell oggetto istanziato da una tale tipologia di struttura. Per generalizzare è possibile anche che esistano più elementi memorizzati nello stesso dizionari con la stessa chiave. In alcuni casi tale comportamento deve essere evitato. Per esempio non possono esistere in un dizionario due persone con lo stesso codice fiscale. In questo caso, quando le chiavi sono uniche nel dizionario, allora si può pensare che esista una specie di mappatura fra le chiavi dei vari elementi e gli indirizzi fisici di memoria in cui tali elementi sono memorizzati. L ADT dizionario Il dato astratto dizionario supporta i seguenti metodi: size() : ritorna il numero degli elementi di D Input: niente Output: numero intero isempty() : Verifica se D è vuoto Input: niente Output: {true, false} elements() : ritorna gli elementi (valori) memorizzati in D Input: niente Output: Iteratore di oggetti (elementi) keys() : ritorna le chiavi memorizzate in D Input: niente Output: Iteratore di oggetti (chiavi) 2

findelement(k) : Se D contiene l elemento con chiave k allora ritorna tale elemento, altrimenti ritorna il valore speciale NO_SUCH_KEY Input: Object(key) Output: Object(elemento) findallelements(k) : Ritorna un iteratore su tutti gli elementi di D aventi chiave k Input: Object(key) Output: Iteratore di oggetti (elementi) insertitem(k,e) : inserisce in D un nuovo elemento di chiave k e valore e Input: Object(key), Object(element) Output: niente removeelement(k) : Se D contiene un elemento con chiave k allora rimuove tale elemento e ne ritorna l elemento, altrimenti ritorna il valore speciale NO_SUCH_KEY Input: Object(key) Output: Object(elemento) removeallelements(k) : rimuove tutti gli elementi di chiave k e restituisce un Iteratore a tali elementi Input: Object(key) Output: Iteratore di oggetti (elementi) Quando i metodi findelement() e removeelement() non hanno successo allora entrambi ritornano il valore speciale NO_SUCH_KEY che è esso stesso un elemento, non nel senso di valore. Un elemento di questo tipo prende il nome di sentinella. L utilizzo di sentinelle nei programmi è consuetudine abbastanza comune. In taluni casi, l utilizzo di una sentinella può essere considerato come l unico modo davvero efficace per risolvere determinati tipi di problemi. Il caso in esame è uno di questi. Infatti il problema di come segnalare l insuccesso di un metodo di ritrovamento di un elemento potrebbe essere segnalato lanciando un eccezione. Tuttavia tale metodologia presenta il grande svantaggio di doversi trovare a gestire su un altra parte del codice tale eccezione. Oltretutto la gestione delle eccezioni richiede molte più risorse che il semplice utilizzo di un elemento (ks,es) speciale come il NO_SUCH_KEY. Si potrebbe pure essere portati a pensare di poter ritornare un riferimento nullo come valore di ritorno dei due metodi chiamati. Tuttavia tale soluzione presenta lo svantaggio, come descritto più avanti, di confondere valori nulli con il fatto di non aver portato a termine con successo il metodo in questione. I metodi findelement(k) e removeelement(k), nel caso siano presenti più elementi con la stessa chiave, non fanno altro che ritornare il primo elemento trovato con tale chiave. La figura seguente mostra un esempio di come agiscono i metodi dell ADT Dizionario sull insieme degli elementi forniti nell esempio. 3

Test di uguaglianza delle chiavi Ciascun dizionario standard richiede, per come è stato definito, che esista un meccanismo per decidere se due chiavi di due elementi distinti di uno stesso dizionario siano uguali oppure no. Tale meccanismo, nel caso il Dizionario sia ordinato, viene fornito dal comparatore associato al dizionario. In caso contrario l oggetto equality tester è lo strumento utilizzato per lo scopo. Tale oggetto supporta il metodo areequal() per stabilire se due chiavi sono uguali oppure no. In questo senso un comparatore può essere visto come una estensione dell equality tester. ADT Dizionari implementati in JAVA Il pacchetto standard java.util include una interfaccia della struttura dati astratta Dizionario, chiamata java.util.map. In più, in questo pacchetto è anche presente una classe astratta, che non implementa completamente tutti i metodi, chiamata java.util.dictionary. Tale classe implementa l interfaccia java.util.map. Tali classi possono però considerarsi obsolete. Per questi motivi: 1) Le classi Map e Dictionary non hanno metodi che permettono di memorizzare elementi con chiavi uguali. Quindi non c è corrispondenza, per esempio con il metodo findallelements(k) visto prima. 2) Le modalità di effettuazione dei confronti fra le chiavi sono diversi 3) Viene utilizzato il riferimento a null sia come valore di un elemento sia come una sentinella per indicare l insuccesso dell esecuzione di alcuni metodi. I due ultimi motivi sono particolarmente importanti. Modalità di confronto delle chiavi con la classe java.util.map La classe java.util.map sottintende che l oggetto chiave contenuto negli elementi del Dizionario implementi l interfaccia Comparable e quindi abbia in se il metodo od i metodi per il confronto dei valori delle chiavi. Questo approccio, se da un lato appare molto comodo, dall altro ha la notevole limitazione di dover fornire dei metodi di comparazione univocamente legati al tipo di chiave utilizzato nel Dizionario. Ma in taluni contesti, le chiavi non possono conoscere quale è il giusto criterio di comparazione. Infatti, utilizzando tali chiavi in algoritmi diversi, le regole possono essere diverse. Per esempio, considerando come chiavi le coordinate di un punto, per alcuni algoritmi l eguaglianza di due chiavi sono rappresentate da punti avente la stessa ascissa x. Altri invece potrebbero considerare l uguaglianza di due chiavi nel caso l ordinata y di due punti siano le stesse. L utilizzo di un comparatore esterno fornisce un modo generale, riutilizzabile, ed adattabile di determinare l eguaglianza di due chiavi. Utilizzo di null sia come un elemento, sia come una sentinella La terza principale differenza con la struttura dati Dizionario definita precedentemente è che la classe java.util.map utilizza gli oggetti null come valore speciale di ritorno (=sentinella) per metodi che non sono riusciti a portare a termine con successo il loro compito. Tuttavia, sempre la classe Map, permette di memorizzare elementi nulli, cioè riferimenti a null, come elementi di alcune coppie (k,e) chiave-valore appartenenti al dizionario. Tale concessione genera dei pericolosi equivoci nel momento in cui un metodo quale findelement(k) restituisce un riferimento a null. Infatti tale valore potrebbe essere generato per il fatto di non aver trovato alcun elemento con tale 4

chiave, oppure per aver trovato un elemento con valore nullo. Per dirimere ogni dubbio a proposito esiste il metodo containskey(k) della classe Map che restituisce il valore booleano true nel caso il dizionario contenga un elemento con tale chiave. Metodi corrispondenti Ecco lo schema completo delle corrispondenze fra le classi java.util.map e l ADT Dizionario. Log Files Un metodo molto semplice di realizzare un Dizionario è quello di utilizzare un vettore non ordinato di elementi, una lista oppure in generale una sequenza di elementi che memorizzi coppie (k,e) chiave-elemento una dopo l altra. Una tale implementazione prende il nome di log file (file di registro) o audit trail. I file di registro sono molto utilizzati soprattutto in applicazioni finanziarie e laddove occorra tener traccia di ogni genere di operazione che venga compiuta su una banca dati o su una struttura riportante informazioni critiche. L importanza di uno strumento simile la si capisce nel momento in cui avviene un crash di sistema o si verifica un problema di qualsiasi natura durante lo svolgimento di una operazione delicata. In questo caso esistono dei meccanismi di sicurezza che, andando a leggere le operazioni svolte prima e durante il problema, riescono a riportare il sistema alla situazione iniziale. 5

Da come è stato presentato si capisce che il file log è una struttura che, data la bassa probabilità che si verifichi un problema, deve permettere inserimenti in sequenza con una elevata efficienza a scapito di una bassa efficienza di recupero delle informazioni. Formalmente si dice che i files di log implementano l ADT dizionario D utilizzando una sequenza S per memorizzare gli elementi di D in un ordine arbitrario. Si assume che l implementazione della sequenza S sia fatta tramite l utilizzo di un Vettore o di una lista doppiamente concatenata. Si può parlare di log file anche come una implementazione di una sequenza non ordinata (unorderd sequenze implementation). Analisi di una struttura dati di tipo log file Lo spazio richiesto per una struttura di questo tipo è dell ordine di Θ(n) e quindi l utilizzo della memoria di queste strutture è proporzionale al numero di elementi memorizzati. Per quanto riguarda il tempo, i log files hanno metodi di inserimento degli elementi molto veloci, dell ordine di O(1). Questo a scapito di tempi di ricerca molto lunghi, dato che per trovare un singolo elemento di chiave k in questa struttura, bisogna visitare tutti gli elementi dal primo fino all elemento trovato o fino alla fine, in caso di insuccesso. Così la complessità di un metodo di ricerca di un elemento avrà in media una complessità dell ordine di O(n). Lo stesso si può dire per tutti i metodi di ricerca (findallelements Θ(n)) e rimozione degli elementi (removeelement(k) O(n), removeallelements(k) Θ(n)). HASH TABLES Le parole contenute in un dizionario sono tutte formate da combinazioni di lettere dell alfabeto. Naturalmente non tutte le combinazioni di lettere formano delle parole sensate. Anzi, le parole prive di significato sono molte più di quelle contenute in un dizionario di Italiano. Si pensi per un attimo di avere a disposizione un vettore contenente tante celle quante sono le possibili combinazioni di lettere. Allora, ordinando tutte le possibili associazioni di lettere in ordine alfabetico, sarebbe possibile associare ad ogni parola un indirizzo di una cella di tale vettore. La cosa sorprendente è che tale numerazione permetterebbe di raggiungere ogni possibile parola in un tempo molto piccolo, dell ordine di O(1). Infatti sarebbe molto semplice ricavarsi una regola per associare, data una qualsiasi combinazione finita di lettere, un indice di tale vettore a tale parola. Quindi, anche le operazioni di inserimento e di rimozione degli elementi nel vettore sarebbero tutte operazioni molto veloci. 6

L esempio riportato fa capire le potenzialità ed i limiti di una tale tecnica. Infatti una realizzazione di questo tipo, anche se molto veloce, occuperebbe una quantità di memoria enorme, dato che le parole di un dizionario formano un numero molto più piccolo (circa 20 000) di tutte le possibili combinazioni di lettere (20 21!). La cosa potrebbe essere risolta conoscendo una regola per cui, ad ogni parola del dizionario, sarebbe possibile associare univocamente un numero intero da 0 al massimo numero di parole presenti nel dizionario. Questa soluzione permetterebbe di non sprecare neppure una locazione di memoria e consentirebbe di avere prestazioni di ricerca, di inserimento e di estrazione dell ordina di O(1). Per sfortuna, una tale situazione non esiste nella pratica. Pur tuttavia l esempio suggerisce che è possibile sfruttare tecniche simili per ricavare indirizzi o indici di un vettore su cui memorizzare gli elementi. Uno dei modi più efficienti di implementare l ADT dizionario è attraverso l utilizzo delle tabelle di hashing (=spezzettare). Tali tabelle utilizzano le tecniche di compressione dei valori delle chiavi visti nell esempio. E meglio andare con ordine. Il vettore di secchi (bucket array) Un bucket array (letteralmente: array di secchi) è un vettore A[] di dimensione N, dove ogni cella di A[] può essere pensata come un secchio (bucket) ovverosia un contenitore di una coppia (k, e) chiave-elemento del dizionario D e dove N definisce la capacità del vettore. Se le chiavi degli elementi fossero dei numeri interi compresi fra 0 e N-1, e le chiavi fossero anche ben distribuite nell insieme [0, N-1], allora, immaginando di avere un solo elemento per chiave, questa sequenza di contenitori rappresenterebbe tutto ciò che occorre per memorizzare gli elementi nel Dizionario. Infatti un elemento e di chiave k sarebbe semplicemente memorizzato nel vettore A[] in posizione A[k]. Le chiavi associate ad elementi nulli semplicemente indicherebbero la posizione del vettore in cui inserire uno speciale elemento sentinella NO_SUCH_KEY. Naturalmente se le chiavi non fossero nulle, potrebbero verificarsi delle collisioni, nel senso che due elementi diversi con la stessa chiave potrebbero fare riferimento ad un unico indirizzo. Ma visto che una cella del vettore o secchio può contenere un solo elemento, bisognerebbe pensare ad una strategia per gestire il problema. Problema che prende il nome di gestione delle collisioni. Naturalmente la cosa migliore da fare è evitare che le collisioni si verifichino, dato che è sempre meglio prevenire che curare. Analisi di una struttura a bucket array Nel caso le chiavi siano uniche, le collisioni non rappresenterebbero un problema, e quindi le ricerche, gli inserimenti e le estrazioni di elementi occuperebbero un tempo che, nel peggiore dei casi, sarebbe dell ordine di O(1). Questo fatto appare come una grande risultato. Raggiunto però a scapito della risorsa memoria. Infatti per memorizzare gli elementi del dizionario occorrerebbero comunque Θ(n) spazi di memoria, anche nel caso che nel dizionario non fosse presente alcun 7

elemento. Nel caso N fosse molto grande, questa implementazione rappresenterebbe un enorme spreco di memoria. Un altro problema di questo tipo di implementazione è rappresentato dal fatto che le chiavi di tutti gli elementi devono essere diverse fra loro. Dato che questi due problemi sono molto comuni, si definisce come struttura dati di tipo hash table l associazione del bucket array assieme ad una buona funzione di mappatura delle chiavi degli elementi nell intervallo [0, N-1]. Funzioni di hashing La seconda parte di una struttura ad hash table, è una funzione h, detta funzione di hash, che mappa le chiavi delgi elementi del dizionario in un intervallo di interi da 0 a N-1, dove N è la capacità del vettore di secchi per questa tabella. Atraverso una tale funzione h è possibile applicare il metodo del bucket array a chiavi arbitrarie. L idea di questa strategia è di utilizzare una funzione h(k) in modo tale da permettere l associazione di una qualsiasi chiave k di un elemento di un dizionario ad un valore h(k) intero compreso nell intervallo [0, N-1]. Si può dire che una tale funzione h() è tanto migliore quanto riesce ad evitare le collisioni di elementi diversi del dizionario. Naturalmente, per ragioni pratiche, la funzione h() deve anche essere la più semplice possibile da calcolare. Seguendo queste due considerazioni la funzione h() può essere suddivisa in due fasi: 1) mappatura di una qualsiasi chiave (in generale di tipo Object) ad un valore numerico intero; 2) mappatura di tale valore intero in un altro compreso nell intervallo [0,N-1]. La prima mappatura si chiama codifica hash, la seconda mappa di compressione. Codifica Hash La prima azione che una funzione di hash deve compiere è quella di prendere una arbitraria chiave k di un qualsiasi elemento del dizionario e di assegnare ad essa un valore intero. Non importa se questo valore è positivo o negativo, se è compreso nell intervallo [0, N-1] o meno. L importante è che siano evitate il più possibile le collisioni fra chiavi diverse. In più, dovrebbe capitare che chiavi 8

considerate uguali dai metodi dell oggetto comparatore o equality tester forniti col dizionario, fossero mappate sullo stesso valore numerico. Funzioni di Hash in Java La generica classe Object definita in Java supporta il metodo hashcode(). Tale metodo mappa ogni oggetto istanziato da tale classe in un corrispondente valore numerico a 32 bit (tipo int). Volendo, ogni classe in Java supporta un tale metodo, visto che lo eredita dalla classe generale Object. Tuttavia bisogna stare attenti ad utilizzare tale metodo così come è. Infatti esso non fa altro che mappare l indirizzo di memoria dell oggetto nel corrispondente valore numerico intero. E quindi potrebbe capitare il caso di due oggetti chiave considerati uguali dal metodo areequal() del comparatore, ma mappati su due valori numerici completamente diversi, solamente per il fatto di essere memorizzati su due locazioni di memoria completamente differenti. Per fortuna che la classe String sovrascrive il metodo hashcode() per fornirne uno più consono alle aspettative di chi vuole implementare un dizionario. Comunque ci sono molti metodi per realizzare delle funzioni di mappatura in modo molto più appropriato. Casting ad un intero Il primo metodo molto semplice per realizzare una funzione di hash è quello di forzare la variabile oggetto chiave ad essere del tipo intero. Così per esempio tutti i tipi interi byte, short, int ed anche per il tipo char si ottiene un ottima funzione di mappatura. Invece per chiavi di tipo float un tale modo di procedere potrebbe essere poco efficace, per il fatto che il casting ad un intero di un numero in virgola mobile porterebbe alla perdita delle cifre decimali del numero stesso, facendo in modo così di mappare chiavi diverse sullo stesso valore intero. Per fortuna è possibile utilizzare come funzione di mappatura il metodo della classe Float floattobits(x) che converte un numero di tipo float nella corrispondente rappresentazione intera del valore dei suoi bits. 9

Sommare componenti Per valori di tipo long e double, la cui rappresentazione occupa il doppio di bits utilizzati per la rappresentazione di valori float, il metodo precedente non risulterebbe praticabile. La soluzione è rappresentata dal forzare il numero corrispondente ottenuto come rappresentazione dei suoi bits ad essere di tipo intero (int). Ma ci si accorge subito comunque del fatto che una tale soluzione favorisce la perdita di informazione di alcuni bits. Così come il metodo alternativo: che somma ai primi bits della corrispondente rappresentazione intera di un long i successivi bits meno significativi di tale rappresentazione attraverso un operazione di shift degli stessi bits. A parole non ci si spiega bene come a mostrarne un esempio: static int hashcode(long i) //long = 64 bit { return (int)((i >>32) + (int)i ); } Lo schema presentato può essere applicato ad ogni tipo di rappresentazione binaria di oggetti la cui rappresentazione binaria del loro valore consiste in un insieme (x 0, x 1,, x k-1 ) di numeri interi. Una funzione valida potrebbe essere questa: h(x)= i= 0 ma sono argomenti affrontati più avanti, dare importanza anche alla posizione di ogni intero, cosa che la sommatoria precedente non fa. Ancora, la rappresentazione di un numero in virgola mobile può essere rappresentato come somma dei numeri ottenuti trasformando la mantissa e l esponente. k 1 x i. Sarebbe necessario però in questo caso, Funzioni di hash polinomiali La somma dei valori interi delle corrispondenti rappresentazioni binarie dei singoli gruppi di bytes può essere estesa anche nel caso delle stringhe. Infatti, pensando che ogni carattere ha una particolare rappresentazione binaria ad uno o due byte (caratteri Unicode), è possibile effettuare la somma dei valori interi di tale rappresentazione, così come mostrato in precedenza. Per esempio la stringa roma si trasforma nel corrispondente valore: 3 x= roma h(x)= x i in cui x 0 è il valore intero della corrispondente rappresentazione i= 0 della lettera r, x 1 della lettera o, e così via Tale tipo di metodologia soffre però del fatto che chiavi diverse composte esattamente dagli stessi caratteri permutati fra di loro vengono mappati nello stesso valore finale. E il caso della chiave amor o ancora della chiave mora. Non è una bella soluzione. Per risolvere il problema è necessario introdurre dei pesi da associare a ciascuna rappresentazione intera del carattere corrispondente a seconda di dove si trovi sul testo: 10

x 0 a k-1 + x 1 a k-2 + + x k-2 a + x k-1 = x k-1 + a(x k-2 + a(x k-3 + + a(x 2 + a(x 1 + a x 0 )) )) Questo particolare tipo di rappresentazione prende il nome di funzione di codifica polinomiale (polynomial hash code). Intuitivamente si può pensare che la funzione di hash utilizzi la moltiplicazione della costante a come un mezzo per fare spazio per ogni nuovo componente della tupla di valori mantenendo al tempo stesso la caratterizzazione data dal componente precedente. Utilizzando una rappresentazione finita per la chiave intera ottenuta, chiaramente si produrrà una qualche perdita di informazione dovuta agli overflow che si verificheranno durante il calcolo della sommatoria pesata. Si sceglie semplicemente di ignorare tali overflow e di prendere solamente il risultato ottenuto. Si è anche notato come la scelta di un buon valore della costante moltiplicativa a sia decisiva per ottenere ottimi risultati. Prove effettuate su circa 50'000 parole del dizionario inglese, mostrano come l utilizzo di valori di a ugualia 33, 37, 39 e 41 producano al massimo solamente 7 collisioni! Codici di hash a traslazione ciclica (cyclic shift hash code) Una variante della rappresentazione polinomiale è realizzabile sostituendo l utilizzo della costante moltiplicativa a con delle operazioni di traslazioni (shift) dei bit delle corrispondenti rappresentazioni numeriche dei singoli elementi x i della rappresentazione polinomiale. L esempio seguente mostra un algoritmo per mappare una stringa in un corrispondente valore intero: static int hashcode(string s) { int h = 0; //variabile a 32 bit for (int i = 0; i < s.lenght(); i ++) { h = (h << 5) (h >>> 27); // shift a 5 bit ciclico h += (int) s.charat(i); // aggiunta del successivo carattere } return h; } La figura seguente illustra meglio il concetto di traslazione di 5 posizioni dei bit di un numero binario: La tabellina seguente illustra come, scegliendo con accortezza l entità delle traslazioni dei singoli bit sia possibile ottenere risultati eccellenti. La prima colonna indica l entità della traslazione dei singoli bit. La seconda quante collisioni si sono verificate in tutto il dizionario, la terza qual è stato il numero massimo di collisioni per un singolo valore h() della funzione di hashing. 11