Dizionario in un array Prestazioni di un dizionario Prestazioni di un

Fondamenti di Informatica 1
Settimana 9
Dizionario in un array
✔ Un dizionario può anche essere realizzato
con un array
Dizionario
– ogni cella dell’array contiene un riferimento ad
una coppia chiave/valore
• un oggetto di tipo Pair
✔ Ci sono due strategie possibili
– mantenere le chiavi ordinate nell’array
– mantenere le chiavi non ordinate nell’array
2
1
Dizionario in un array ordinato
Dizionario in un array non ordinato
✔ Se le n chiavi vengono conservate in ordine
✔ Se le n chiavi vengono conservate disordinate
nell’array
– la ricerca ha prestazioni O(log n)
• si può usare la ricerca per bisezione
– l’inserimento ha prestazioni O(n)
• si usa l’ordinamento per inserzione in un array
ordinato
• con altre strategie, occorre invece ordinare l’intero
array, con prestazioni, in generale, O(n log n)
– la rimozione ha prestazioni O(n)
• bisogna fare una ricerca, e poi spostare mediamente
n/2 elementi per mantenere l’ordinamento
3
Prestazioni di un dizionario
Dizionario
lg n
n
lg n
inserimento
n
1
lg n
rimozione
n
n
lg n
• bisogna usare la ricerca lineare
– l’inserimento ha prestazioni O(1)
• è sufficiente inserire il nuovo elemento nell’ultima
posizione dell’array
– la rimozione ha prestazioni O(n)
• bisogna fare una ricerca, e poi spostare nella
posizione trovata l’ultimo elemento dell’array,
perché l’ordinamento non interessa
4
✔ La scelta di una realizzazione piuttosto di un’altra
dipende dall’utilizzo tipico del dizionario
nell’applicazione
✔ Ad esempio
5
Marcello Dalpasso
– la ricerca ha prestazioni O(n)
Prestazioni di un dizionario
array array non ABR
ordinato ordinato bilanciato
ricerca
nell’array
– se nel dizionario di fanno frequenti inserimenti e
sporadiche ricerche e rimozioni
• la scelta più opportuna è l’array non ordinato
– se i tipi di operazioni hanno la stessa frequenza
• la scelta più opportuna è l’albero binario di ricerca
– se il dizionario viene costruito una volta per tutte, poi
viene usato per fare soltanto ricerche
• la scelta più opportuna è l’array ordinato, che sfrutta
la memoria in modo più efficiente dell’ABR
6
1
Fondamenti di Informatica 1
Settimana 9
Chiavi numeriche
Tabella
✔ Se imponiamo una restrizione al campo di
✔ Un dizionario con chiavi numeriche intere viene
detto tabella o tavola (table)
applicazione di un dizionario
✔ L’analogia è la seguente
– un valore è una riga nella tabella
– le righe sono numerate usando le chiavi
– alcune righe possono essere vuote (senza valore)
– supponiamo che le chiavi siano numeri interi
appartenenti ad un intervallo noto a priori
si può realizzare molto semplicemente un
dizionario con prestazioni O(1) per tutte le
operazioni
✔ Si usa un array che contiene soltanto i riferimenti
ai valori, usando le chiavi come indici nell’array
– le celle dell’array che hanno come indice una chiave
che non appartiene al dizionario hanno il valore null
7
Tabella
✔ Definiamo il tipo di dati astratto Table con un
comportamento identico al dizionario
– l’unica sostanziale differenza è che le chiavi non sono
riferimenti ad oggetti di tipo Comparable, ma sono
numeri interi (che evidentemente sono confrontabili)
public interface Table extends Container
{ void insert(int key, Object value);
void remove(int key);
Object find(int key);
}
9
chiave = 0 valore0
null
chiave = 2 valore2
chiave = 3 valore3
null
null
dato
dato
dato
public class ArrayTable100 implements Table
{ private Object[] v = new Object[100];
private int count = 0; // count rende isEmpty O(1)
public void makeEmpty()
{ count = 0;
for(int i = 0; i < v.length; i++)v[i] = null; }
public boolean isEmpty() { return count == 0; }
private void check(int key)
{ if (key < 0 || key >= v.length)
throw new InvalidPositionTableException(); }
public void insert(int key, Object value)
{ check(key);
if (v[key] == null) count++;
v[key] = value; }
public void remove(int key)
{ check(key);
if (v[key] != null) count--;
v[key] = null; }
public Object find(int key)
{ check(key);
return v[key];
}
}
Tabella
Tabella
✔ La tabella potrebbe avere dimensione variabile,
✔ La tabella non utilizza la memoria in modo
cioè utilizzare un array di dimensione crescente
quando sia necessario
– l’operazione di inserimento richiede, però, un tempo
O(n) ogni volta che è necessario un ridimensionamento
– in questo caso non si può utilizzare l’analisi
ammortizzata perché non si può prevedere quali siano
le posizioni richieste dall’utente
– non è più vero che il ridimensionamento avviene “una
volta ogni tanto”, può avvenire anche tutte le volte
– le prestazioni nel caso peggiore sono quindi O(n)
11
Marcello Dalpasso
8
10
efficiente
– l’occupazione di memoria richiesta per contenere n dati
non dipende da n in modo lineare
• come invece avviene per tutti gli altri ADT
ma dipende dal contenuto informativo presente nei dati
• in particolare, dal valore della chiave massima
✔ Può essere necessario un array di milioni di
elementi per contenere poche decine di dati
– si definisce fattore di riempimento (load factor) della
tabella il numero di dati contenuti nella tabella diviso
per la dimensione della tabella stessa
12
2
Fondamenti di Informatica 1
Settimana 9
Tabella
✔ La tabella è un dizionario con prestazioni ottime
– tutte le operazioni sono O(1)
ma con le seguenti limitazioni
– le chiavi devono essere numeri interi (non negativi)
• in realtà si possono usare anche chiavi negative,
sottraendo ad ogni chiave il valore dell’estremo
inferiore dell’intervallo di variabilità
– l’intervallo di variabilità delle chiavi deve essere noto
a priori
• per dimensionare la tabella
– se il fattore di riempimento è molto basso, si ha un
grande spreco di memoria
• ciò avviene se le chiavi sono molto “disperse” nel
loro insieme di variabilità
13
Intervallo
14
Tabella
Funzione di hash
✔ Cerchiamo di eliminare una delle limitazioni
✔ Una funzione di hash ha
– come dominio l’insieme delle chiavi che identificano
univocamente i dati da inserire nel dizionario
– come codominio l’insieme degli indici validi per
accedere ad elementi della tabella
• il risultato dell’applicazione della funzione di hash
ad una chiave si chiama chiave ridotta
✔ Se manteniamo la limitazione di usare numeri
– l’intervallo di variabilità delle chiavi deve
essere noto a priori
• per dimensionare la tabella
✔ Risolviamo il problema in modo diverso
– fissiamo la dimensione della tabella in modo
arbitrario
• in questo modo si definisce di conseguenza
l’intervallo di variabilità delle chiavi
utilizzabili
– per usare chiavi esterne all’intervallo, si usa una
funzione di trasformazione delle chiavi
• funzione di hash
interi come chiavi
– una semplice funzione di hash è il calcolo del resto
della divisione intera tra la chiave e la dimensione
della tabella
15
16
Funzione di hash
Funzione di hash
✔ Per come è definita, la funzione di hash è
✔ Il fatto che la funzione di hash non sia univoca
generalmente non biunivoca, cioè non è
invertibile
genera un problema nuovo
– chiavi diverse possono avere lo stesso valore per la
funzione di hash
✔ Per questo si chiama funzione di hash
– è una funzione che “fa confusione”, nel senso che
“mescola” dati diversi…
✔ In generale, non è possibile definire una funzione
di hash biunivoca, perché la dimensione del
dominio è maggiore della dimensione del
codominio
Marcello Dalpasso
17
– inserendo un valore nella tabella, può darsi che la sua
chiave ridotta sia uguale a quella di un valore già
presente nella tabella e avente una diversa chiave, ma la
stessa chiave ridotta
– usando l’algoritmo già visto per la tabella, il nuovo
valore andrebbe a sostituire il vecchio
• questo non è corretto perché i due valori hanno, in
realtà, chiavi diverse
– questo fenomeno si chiama collisione nella tabella e si
può risolvere in molti modi diversi
• una tabella che usa chiavi ridotte si chiama tabella
18
hash (hash table)
3
Fondamenti di Informatica 1
Settimana 9
Risoluzione delle collisioni
Risoluzione delle collisioni
✔ Quando si ha una collisione, bisognerebbe inserire
✔ Questo sistema di risoluzione delle collisioni si
il nuovo valore nella stessa cella della tabella
(dell’array) che già contiene un altro valore
– i due valori hanno chiavi diverse, ma la stessa chiave
ridotta
– ciascun valore dovrebbe essere memorizzato insieme
alla sua vera chiave, per poter fare le ricerche nel modo
corretto
✔ Possiamo risolvere il problema usando una lista
per ogni cella dell’array
– l’array è un array di riferimenti a liste
– ciascuna lista contiene le coppie chiave/valore che
hanno la stessa chiave ridotta
chiama tabella hash con bucket
– un bucket è una delle liste associate ad una
chiave ridotta
hash = 0 bucket0
null
hash = 2 bucket2
hash = 3 bucket3
null
null
coppia
coppia
coppia
coppia
coppia
19
20
Tabella hash con bucket
Tabella hash con bucket
✔ Le prestazioni della tabella hash con bucket non
✔ Le prestazioni dipendono fortemente dalle
sono più, ovviamente, O(1) per tutte le operazioni
✔ Le prestazioni dipendono fortemente dalle
caratteristiche della funzione di hash
– caso peggiore: la funzione di hash restituisce sempre la
stessa chiave ridotta, per ogni chiave possibile
– tutti i dati vengono inseriti in un’unica lista
• le prestazioni della tabella hash degenerano in quelle
di una lista
• tutte le operazioni sono O(n)
21
caratteristiche della funzione di hash
– caso migliore: la funzione di hash restituisce chiavi
ridotte che si distribuiscono uniformemente nella
tabella
– tutte le liste hanno la stessa lunghezza media
• se M è la dimensione della tabella
– la lunghezza media di ciascuna lista è n/M
• tutte le operazioni sono O(n/M)
– per avere prestazioni O(1) occorre dimensionare la
tabella in modo che M sia dello stesso ordine di
22
grandezza di n
Tabella hash con bucket
Tabella hash con chiave generica
✔ Riassumendo, in una tabella hash con bucket si
✔ Rimane da risolvere un solo problema
– le chiavi devono essere numeri interi (non negativi)
✔ Vogliamo cercare di realizzare una tabella hash
ottengono prestazioni ottimali (costanti) se
– la dimensione della tabella è circa uguale al numero di
dati che saranno memorizzati nella tabella
• fattore di riempimento circa unitario
– così si riduce al minimo anche lo spreco di memoria
– la funzione di hash genera chiavi ridotte uniformemente
distribuite
• liste di lunghezza quasi uguale alla lunghezza media
• le liste hanno quasi tutte lunghezza uno!
✔ Se le chiavi vere sono uniformemente distribuite
– la funzione di hash che “calcola il resto della
divisione intera” genera chiavi ridotte
23
uniformemente distribuite
Marcello Dalpasso
con bucket che possa gestire coppie chiave/valore
in cui la chiave non è un numero intero, ma un
dato generico
– ad esempio, una stringa
– applicazione
• contenitore di oggetti di tipo PersonaFisica
• chiave: codice fiscale
24
4
Fondamenti di Informatica 1
Settimana 9
Tabella hash con chiave generica
Funzione di hash per chiave generica
✔ Se vogliamo usare chiavi generiche (qualsiasi tipo
di oggetto, in teoria…)
– è sufficiente progettare una adeguata funzione
di hash
• se la funzione di hash ha come dominio
l’insieme delle chiavi generiche che
interessano e come codominio un
sottoinsieme dei numeri interi
– cioè se le chiavi ridotte sono numeri
interi
allora la tabella hash con bucket funziona
correttamente senza modifiche
✔ Come si può trasformare una stringa in un numero intero?
✔ Ricordiamo il significato della notazione posizionale
nella rappresentazione di un numero intero
434 = 4·102 + 3·101 + 4·100
✔ Ciascuna cifra rappresenta un numero che dipende
– dal suo valore intrinseco
– dalla sua posizione
✔ Possiamo usare la stessa convenzione per una
stringa, dove ciascun carattere rappresenta un
numero che dipende
– dal suo valore intrinseco come carattere
• nella codifica Unicode
– dalla sua posizione nella stringa
25
Funzione di hash per chiave generica
26
Funzione di hash per chiave generica
✔ Possiamo usare la stessa convenzione per una
27
✔ Come fare per oggetti generici?
– la classe Object mette a disposizione il metodo
hashCode che restituisce un int con buone proprietà di
distribuzione uniforme
– se il metodo hashCode non viene ridefinito, viene
calcolata una chiave ridotta a partire dall’indirizzo
dell’oggetto in memoria
– l’esistenza di questo metodo rende possibile l’utilizzo
di qualsiasi oggetto come chiave in una tabella hash
• invocando hashCode si ottiene un valore di tipo int
– calcolando il resto della divisione intera di tale
valore per la dimensione della tabella, si ottiene
finalmente la chiave ridotta
28
29
30
stringa, dove ciascun carattere rappresenta un
numero che dipende
– dal suo valore intrinseco come carattere
• nella codifica Unicode
– dalla sua posizione nella stringa
✔ La base del sistema sarà il numero di simboli diversi
(come nella base decimale), che per la codifica
Unicode è 65536
"ABC" ⇒ 'A'·655362 + 'B'·655361 + 'C'·655360
✔ In Java è molto semplice, perché un char è anche un
numero intero...
Tabella hash per chiave generica
public interface HashTable extends Container
{ void insert(Object key, Object value);
void remove(Object key);
Object find(Object key);
}
Marcello Dalpasso
5
Fondamenti di Informatica 1
Settimana 9
Attraversamento di un albero
✔ Abbiamo visto un metodo statico per attraversare
un albero, ad esempio in ordine anticipato
Classi astratte
public static void preOrderTraversal(BinaryTree t)
{ if (t == null) return;
visit(t);
preOrderTraversal(t.left);
preOrderTraversal(t.right);
}
✔ Con una realizzazione di questo tipo, ogni volta
che abbiamo bisogno di realizzare un
attraversamento occorre copiare il codice del
metodo all’interno della classe
31
– alternativa: inseriamo il metodo in una classe
contenitore di metodi
32
Attraversamento di un albero
Attraversamento di un albero
✔ Inseriamo il metodo in una classe
✔ In questo modo, il metodo di attraversamento può
essere invocato da qualsiasi classe
– non c’è bisogno di copiare codice…
public class TreeTraversal
{ public void preOrder(BinaryTree t)
{ if (t == null) return;
visit(t);
preOrder(t.left);
preOrder(t.right);
}
public void postOrder(BinaryTree t)
{ ... }
public void inOrder(BinaryTree t)
{ ... }
}
BinaryTree t = new BinaryTree();
...
TreeTraversal trav = new TreeTraversal();
trav.preOrder(t);
✔ Purtroppo, la compilazione della classe
TreeTraversal non va a buon fine!
– manca la definizione del metodo visit
33
Attraversamento di un albero
Attraversamento di un albero
✔ Abbiamo detto che l’attraversamento è un azione
generica di trasformazione di una struttura
gerarchica (l’albero) in una sequenza
– indipendentemente dall’elaborazione che viene
svolta dal metodo visit su ciascuna etichetta che
costituisce la sequenza
✔ Possiamo quindi definire diverse classi, usando
TreeTraversal come “prototipo”, definendo in
modo diverso al loro interno il metodo visit
35
Marcello Dalpasso
34
// attraversamenti che visualizzano le
// etichette presenti nell’albero
public class PrintTreeTraversal
{ public void preOrder(BinaryTree t)
{ if (t == null) return;
visit(t);
preOrder(t.left);
preOrder(t.right);
}
public void postOrder(BinaryTree t)
{ ... }
public void inOrder(BinaryTree t)
{ ... }
private void visit(BinaryTree t)
{ System.out.println(t.label);
}
}
36
6
Fondamenti di Informatica 1
Settimana 9
Attraversamento di un albero
Attraversamento di un albero
// attraversamenti che rendono nulle le
// etichette presenti nell’albero
public class NullTreeTraversal
{ public void preOrder(BinaryTree t)
{ if (t == null) return;
visit(t);
preOrder(t.left);
preOrder(t.right);
}
public void postOrder(BinaryTree t)
{ ... }
public void inOrder(BinaryTree t)
{ ... }
private void visit(BinaryTree t)
{ t.label = null;
}
}
✔ Le due classi che abbiamo appena visto
differiscono soltanto per la definizione del metodo
visit
– c’è molto codice condiviso (codice da copiare…)
✔ In Java è possibile definire una classe che
contenga soltanto il codice in comune
– tale classe non è completa!
• manca il metodo visit
– però può essere usata come superclasse per derivare
una classe completa senza copiare codice
37
Classe astratta
Classe astratta
public abstract class TreeTraversal
{ public void preOrder(BinaryTree t)
{ if (t == null) return;
visit(t);
preOrder(t.left);
preOrder(t.right); }
public void postOrder(BinaryTree t)
{ ... }
public void inOrder(BinaryTree t)
{ ... }
public abstract void visit(BinaryTree t);
}
public class PrintTreeTraversal
extends TreeTraversal
{ public void visit(BinaryTree t)
{ System.out.println(t.label);
}
}
✔ Una classe astratta ha le seguenti caratteristiche
– contiene la dichiarazione abstract prima di class
– può contenere metodi di cui viene definita soltanto la
firma
• tali metodi devono essere dichiarati abstract
– la classe può essere compilata
– non si possono costruire esemplari della classe
• ma si possono costruire esemplari di classi non
astratte derivate da essa
✔ Una classe derivata da una classe astratta (se non è
essa stessa astratta) ha le seguenti caratteristiche
– deve contenere la definizione di tutti i metodi astratti
della sua superclasse
39
Classe astratta o interfaccia?
40
Come fare senza classi astratte?
✔ La classe astratta è simile ad un’interfaccia, in
quanto demanda ad un’altra classe la realizzazione
concreta (di una parte) del suo comportamento
✔ Diversamente dall’interfaccia, però, la classe
astratta definisce concretamente una parte del
comportamento che sarà ereditato dalle classi
derivate
– in un’interfaccia, invece, non viene concretamente
definito niente
public class TreeTraversal
{ public void preOrder(BinaryTree t)
{ if (t == null) return;
visit(t);
preOrder(t.left);
preOrder(t.right); }
public void postOrder(BinaryTree t)
{ ... }
public void inOrder(BinaryTree t)
{ ... }
public void visit(BinaryTree t)
{ throw new RuntimeException(); }
}
✔ Se non si vogliono usare classi astratte, si ottiene
41
Marcello Dalpasso
38
lo stesso risultato pratico definendo la classe
completa con un metodo non utilizzabile
42
7
Fondamenti di Informatica 1
Settimana 9
Come fare senza classi astratte?
public class TreeTraversal
{ ...
public void visit(BinaryTree t)
{ throw new RuntimeException(); }
}
public class PrintTreeTraversal extends TreeTraversal
{ public void visit(BinaryTree t)
{ System.out.println(t.label);
}
}
Intervallo
✔ In questo modo la classe
– viene compilata correttamente
– i suoi metodi non possono essere invocati,
perché viene lanciata un’eccezione
✔ La classe deve quindi essere necessariamente
estesa, sovrascrivendo il metodo visit
43
44
Realizzazione di una pila “sicura”
✔ Tutte le strutture dati che abbiamo definito
Esercizio
possono contenere oggetti di qualsiasi tipo
Realizzazione di una pila “sicura”
✔ Solitamente vengono utilizzate per contenere
oggetti omogenei
– cioè oggetti che siano tutti dello stesso tipo
ma in generale questo vincolo non c’è
✔ Anche quando vengono usate come contenitori di
oggetti omogenei, è possibile introdurre oggetti
disomogenei, per errore
✔ Vogliamo realizzare una struttura dati, ad esempio
una pila, che possa contenere soltanto oggetti di
un certo tipo
46
45
// rivediamo la definizione della pila
public class ArrayStack implements Stack
{ private Object[] v = new Object[100];
private int vSize = 0;
public boolean isEmpty() { return vSize == 0; }
public void makeEmpty() { vSize = 0; }
public void push(Object obj)
{ if (vSize == v.length) v = resize(v, 2*v.length);
v[vSize++] = obj; }
public Object top()
{ if (isEmpty()) throw new EmptyStackException();
return v[vSize - 1]; }
public void pop()
{ if (isEmpty()) throw new EmptyStackException();
vSize--; }
public Object topAndPop()
{ Object obj = top();
pop();
return obj; }
private Object[] resize(Object[] old, int newLength)
{ Object[] newArray = new Object[newLength];
for (int i = 0; i < old.length; i++)
newArray[i] = old[i];
return newArray; }
}
Marcello Dalpasso
Realizzazione di una pila “sicura”
✔ Per realizzare una pila che possa contenere
soltanto stringhe, ad esempio
– si può riscrivere tutto il codice modificando il solo
metodo push
public class StringArrayStack implements Stack
{ ...
public void push(Object obj)
{ if (!(obj instanceof String))
throw new InvalidTypeException();
if (vSize == v.length)
v = resize(v, 2*v.length);
v[vSize++] = obj; }
}
47
48
8
Fondamenti di Informatica 1
Settimana 9
Realizzazione di una pila “sicura”
Realizzazione di una pila “sicura”
✔ Si potrebbe pensare di evitare il controllo di tipo
✔ Un modo più “elegante” sfrutta l’ereditarietà
– che, come sappiamo, è spesso utile per evitare di
ricopiare codice…
nel metodo modificando il tipo del parametro
public class StringArrayStack implements Stack
{ ... // NON FUNZIONA
public void push(String obj)
{ if (vSize == v.length)
v = resize(v, 2*v.length);
v[vSize++] = obj; }
}
✔ Non funziona: il compilatore segnala che la classe
non definisce tutti i metodi richiesti
dall’interfaccia Stack
– il metodo push ha una firma diversa
49
public class StringArrayStack extends ArrayStack
{ public void push(Object obj)
{ if (!(obj instanceof String))
throw new InvalidTypeException();
super.push(obj);
}
}
✔ Il metodo push viene sovrascritto (si noti la firma
identica…), per cui negli esemplari di
StringArrayStack verrà invocato il metodo qui
definito e non quello di ArrayStack
50
Realizzazione di una pila “sicura”
Realizzazione di una pila “sicura”
✔ Vediamo come utilizzare StringArrayStack
✔ Viene la tentazione di ridefinire il metodo
Stack s = new StringArrayStack();
s.push("pippo");
String st = (String) s.topAndPop();
s.push(new Integer(2)); // lancia un’eccezione
✔ Usando StringArrayStack è possibile fare il cast
esplicito dopo l’estrazione di un elemento con la
certezza che andrà a buon fine
✔ Viene la tentazione di ridefinire il metodo
topAndPop come segue, per non dover fare il cast
{
...
public String topAndPop() { ... }
}
51
topAndPop come segue, per non dover fare il cast
{
...
public String topAndPop() { ... }
}
✔ Questo di nuovo non funziona, per lo stesso
motivo citato in precedenza
– il compilatore segnala che la classe non definisce tutti i
metodi richiesti dall’interfaccia Stack
• il metodo topAndPop ha una firma diversa
52
Realizzazione di una pila “sicura”
✔ Alternativa senza usare l’ereditarietà
– usiamo una pila come variabile privata di esemplare
public class StringArrayStack implements Stack
{ private Stack s = new ArrayStack();
public boolean isEmpty() { return s.isEmpty(); }
public void makeEmpty() { s.makeEmpty(); }
public void push(Object obj)
{ if (!(obj instanceof String))
throw new InvalidTypeException();
s.push(obj); }
public Object top() { return s.top(); }
public void pop() { s.pop(); }
public Object topAndPop()
{ return s.topAndPop(); }
}
53
Marcello Dalpasso
54
9
Fondamenti di Informatica 1
Settimana 9
Insieme
✔ Il tipo di dati astratto “insieme” (set) è un
contenitore (eventualmente vuoto) di
oggetti distinti
Insieme
– senza alcun particolare ordinamento
– senza memoria dell’ordine temporale in cui gli
oggetti vengono inseriti od estratti
– si comporta come un insieme matematico
56
55
Insieme
Insieme
✔ Le operazioni consentite sull’insieme sono
– inserimento di un oggetto
✔ Appartengono all’unione di due insiemi tutti e soli
• fallisce silenziosamente se l’oggetto è già presente
– verifica della presenza di un oggetto
– ispezione di tutti gli oggetti
✔ Per due insiemi A e B, si definiscono le operazioni
– unione, A ∪ B
– intersezione, A ∩ B
– sottrazione, A - B (oppure anche A \ B)
gli oggetti che appartengono ad almeno uno dei
due insiemi
✔ Appartengono all’intersezione di due insiemi tutti
e soli gli oggetti che appartengono ad entrambi gli
insiemi
✔ Appartengono all’insieme sottrazione A-B tutti e
soli gli oggetti che appartengono all’insieme A e
non appartengono all’insieme B
57
Insieme
Insieme con array non ordinato
public interface Set
extends Container
{ void insert(Object obj);
boolean isInSet(Object obj);
Object[] get();
}
✔ Per realizzare un insieme ci sono diverse soluzioni
– array non ordinato
– array ordinato
– albero binario di ricerca
59
Marcello Dalpasso
58
public class ArraySet implements Set
{ private Object[] v = new Object[100];
private int vSize = 0;
public void makeEmpty() { vSize = 0; }
public boolean isEmpty() { return vSize == 0; }
public Object[] get()
// O(n)
{ Object[] x = new Object[vSize];
System.arraycopy(v, 0, x, 0, vSize);
return x; }
public boolean isInSet(Object x) // O(n)
{ for (int i = 0; i < vSize; i++)
if (v[i].equals(x)) return true;
return false; }
public void insert(Object x)
// O(n)
{ if (isInSet(x)) return;
if (vSize == v.length)
v = resize(v, 2*vSize);
v[vSize++] = obj; }
}
60
10
Fondamenti di Informatica 1
Settimana 9
Operazioni su insiemi: unione
public static Set union(Set s1, Set s2)
{ Set x = new ArraySet();
// inseriamo tutti gli elementi del
// primo insieme
Object[] v = s1.get();
for (int i = 0; i < v.length; i++)
x.insert(v[i]);
// inseriamo tutti gli elementi del
// secondo insieme, sfruttando le
// proprieta’ di insert
v = s2.get();
for (int i = 0; i < v.length; i++)
x.insert(v[i]);
return x;
}
Operazioni su insiemi: intersezione
public static Set intersection(Set s1,
Set s2)
{ Set x = new ArraySet();
Object[] v = s1.get();
for (int i = 0; i < v.length; i++)
if (s2.isInSet(v[i]))
x.insert(v[i]);
return x;
}
61
Operazioni su insiemi: sottrazione
public static Set subtract(Set s1,
Set s2)
{ Set x = new ArraySet();
Object[] v = s1.get();
for (int i = 0; i < v.length; i++)
if (!s2.isInSet(v[i]))
x.insert(v[i]);
return x;
}
Insieme con array ordinato
✔ Realizzando un insieme con un array ordinato, si
pone il problema di confrontare gli oggetti tra loro
– nell’insieme non c’è il vincolo che gli oggetti debbano
realizzare l’interfaccia Comparable…
✔ Come si può introdurre una relazione d’ordine
completo in un insieme di oggetti di tipo Object?
✔ Se isInSet (e, di conseguenza, insert) nell’insieme
sono O(n), queste tre operazioni sono
62
O(n2)
✔ Ovviamente, occorre utilizzare soltanto metodi
della classe Object
63
64
Insieme con array ordinato
Insieme con array ordinato
✔ Ad esempio, il metodo toString, se non
✔ Un altro metodo di Object utile in questo contesto
sovrascritto, genera una stringa che contiene
l’indirizzo in memoria dell’oggetto
– tale informazione è diversa per ogni oggetto (è una
chiave)
compareTo(Object x, Object y)
{ return x.toString().compareTo(y.toString());
}
✔ Se il metodo toString viene sovrascritto, ci
possono essere oggetti diversi per i quali viene
generata la stessa stringa…
– conti correnti con lo stesso saldo...
Marcello Dalpasso
è hashCode
– genera un numero intero che, per quanto
possibile, è diverso per ogni oggetto
• non è però garantito che lo sia...
– anch’esso può essere sovrascritto, ma se viene
sovrascritto dovrebbe mantenere le stesse
proprietà
int compareTo(Object x, Object y)
{ return x.hashCode() - y.hashCode();
}
65
66
11
Fondamenti di Informatica 1
Settimana 9
Insieme con array ordinato
Insieme con array ordinato
✔ Una possibile scelta, per aumentare la probabilità
di successo, è l’uso di entrambi i metodi
int compareTo(Object x, Object y)
{ String sx = x.toString() + x.hashCode();
String sy = y.toString() + y.hashCode();
return sx.compareTo(sy);
}
✔ Non esiste, però, la garanzia assoluta che due
oggetti distinti non vengano considerati uguali
– l’inserimento potrebbe fallire, cioè non inserire un
oggetto perché ritenuto erroneamente il duplicato di un
altro oggetto già presente
67
public class SortedArraySet implements Set
{ private Object[] v = new Object[100];
private int vSize = 0;
public void makeEmpty()
{ vSize = 0; }
public boolean isEmpty()
{ return vSize == 0; }
public Object[] get()
// O(n)
{ Object[] x = new Object[vSize];
System.arraycopy(v, 0, x, 0, vSize);
return x; }
private int compareTo(Object x, Object y)
{ ... }
...
}
68
Insieme con array ordinato
public class SortedArraySet implements Set
{ ...
public boolean isInSet(Object x) // O(log n)
{ // si puo’ fare una ricerca binaria
...
}
public void insert(Object x)
// O(n)
{ if (isInSet(x)) return;
if (vSize == v.length)
v = resize(v, 2*vSize);
v[vSize++] = obj;
// usiamo insertion sort che e’ O(n)
// perche’ inseriamo in un array ordinato
...
}
}
Intervallo
69
Operazioni su insiemi
Operazioni su insiemi
✔ Gli algoritmi già visti per le operazioni sugli insiemi
possono essere utilizzati senza alcuna modifica,
anche per l’insieme realizzato con un array ordinato
– la complessità di tutti gli algoritmi (unione,
intersezione e sottrazione) rimane O(n2), perché
il metodo insert è rimasto O(n)
✔ Senza avere informazioni sul funzionamento interno
dell’insieme, non è possibile ottenere prestazioni
migliori...
71
Marcello Dalpasso
70
✔ Usare l’incapsulamento ad oltranza può essere
sconveniente…
✔ In questo caso, sapendo che
– l’array ottenuto con il metodo get è ordinato
– l’inserimento nell’insieme avviene nel metodo
insert usando un algoritmo di ordinamento per
inserzione in un array ordinato, a partire
dall’elemento di indice più elevato
è possibile scrivere versioni più efficienti dei metodi
già visti
72
12
Fondamenti di Informatica 1
Settimana 9
Operazioni su insiemi: unione
Operazioni su insiemi: unione
✔ Per realizzare l’unione, osserviamo che il problema
è molto simile alla fusione di due array ordinati
– come abbiamo visto in MergeSort, questo
algoritmo di fusione è O(n)
✔ L’unica differenza consiste nella contemporanea
eliminazione (cioè nel non inserimento…) di
eventuali oggetti duplicati
– un oggetto presente in entrambi gli insiemi dovrà
essere presente una sola volta nell’insieme unione
73
Operazioni su insiemi: unione
public static Set union(SortedArraySet s1,
SortedArraySet s2)
{ Set x = new SortedArraySet();
Object[] v1 = s1.get();
Object[] v2 = s2.get();
int i = 0, j = 0;
while (i < v1.length && j < v2.length)
if (v1[i].compareTo(v2[j]) < 0)
x.insert(v1[i++]);
else if (v1[i].compareTo(v2[j]) > 0)
x.insert(v2[j++]);
while (i < v1.length)
x.insert(v1[i++]);
while (j < v2.length)
x.insert(v2[j++]);
return x;
} // prestazioni O(n log n) anziche’ quadratiche
secondo l’algoritmo visto in MergeSort, gli oggetti
vengono via via inseriti nell’insieme unione che si
va costruendo
✔ Se gli inserimenti avvengono con oggetti in ordine
crescente, l’ordinamento per inserzione in un array
ordinato che viene usato dal metodo insert ha
prestazioni O(1) per ogni inserimento!
– il metodo insert ha quindi prestazioni O(log n)
• perché invoca isInSet
– se gli inserimenti non avvengono in ordine
crescente, il metodo insert ha prestazioni medie
di tipo O(n)
74
Operazioni su insiemi: intersezione
public static Set intersection(SortedArraySet s1,
SortedArraySet s2)
{ Set x = new SortedArraySet();
Object[] v1 = s1.get();
Object[] v2 = s2.get();
for (int i = 0, j = 0; i < v1.length; i++)
{ while (v1[i].compareTo(v2[j]) > 0) j++;
if (v1[i].compareTo(v2[j]) == 0)
x.insert(v1[i]);
}
return x;
} // prestazioni O(n log n) anziche’ quadratiche
75
Operazioni su insiemi: sottrazione
public static Set subtract(SortedArraySet s1,
SortedArraySet s2)
{ Set x = new SortedArraySet();
Object[] v1 = s1.get();
Object[] v2 = s2.get();
for (int i = 0, j = 0; i < v1.length; i++)
{ while (v1[i].compareTo(v2[j]) > 0) j++;
if (v1[i].compareTo(v2[j]) != 0)
x.insert(v1[i]);
}
return x;
} // prestazioni O(n log n) anziche’ quadratiche
77
Marcello Dalpasso
✔ Effettuando la fusione dei due array ordinati
76
Insieme con ABR
✔ Un insieme può essere realizzato anche usando un
albero binario di ricerca come struttura di
memorizzazione dei dati
✔ Gli algoritmi saranno identici a quelli visti per la
realizzazione con un array ordinato
– il metodo get richiede un attraversamento dell’albero in
ordine simmetrico, che comunque è un’operazione
O(n) come la copiatura dell’array
✔ C’è lo stesso problema visto per gli array…
– se gli oggetti appartenenti all’insieme non sono
Comparable, non si possono inserire nell’ABR
78
13
Fondamenti di Informatica 1
Settimana 9
Insieme con ABR
Insieme con ABR
✔ Possiamo definire una classe involucro che
trasforma oggetti qualsiasi in oggetti
Comparable, usando il metodo compareTo visto
in precedenza
public class MyObject implements Comparable
{ Object x;
public MyObject(Object o)
{ x = o;
}
public int compareTo(Object y)
{ String sx = x.toString() + x.hashCode();
String sy = y.toString() + y.hashCode();
return sx.compareTo(sy);
}
}
public class BSTSet implements Set
{ private BinarySearchTree bst = new BST();
public void makeEmpty()
{ bst.makeEmpty(); }
public boolean isEmpty()
{ return bst.isEmpty(); }
public Object[] get() // O(n)
{ // crea un iteratore per l’ABR
// ed effettua un attraversamento
// in ordine simmetrico
}
...
}
79
Insieme con ABR
80
Operazioni su insiemi
public class BSTSet implements Set
{ ...
public boolean isInSet(Object x) // O(log n)
{ try { bst.find(new MyObject(x)); }
catch (ItemNotFoundException e)
{ return false; }
return true;
}
public void insert(Object x)
// O(log n)
{ if (isInSet(x)) return;
bst.insert(new MyObject(x));
}
}
✔ Gli algoritmi già visti per le operazioni sugli insiemi
possono essere utilizzati senza alcuna modifica,
anche per l’insieme realizzato con un ABR
– la complessità di tutti gli algoritmi (unione,
intersezione e sottrazione) è O(n log n), perché il
metodo insert è O(log n)
✔ Avendo informazioni sul funzionamento interno
dell’insieme, è possibile ottenere prestazioni
migliori?
– la risposta è negativa
81
82
Esercizio
Tipo di dati duplice
83
Marcello Dalpasso
84
14
Fondamenti di Informatica 1
Settimana 9
Strutture dati complesse
Strutture dati complesse
✔ Abbiamo visto diversi tipi di dati astratti (ADT)
– pila, coda, lista, albero binario, albero binario di ricerca,
dizionario, tabella, tabella hash, insieme
✔ Una classe Java può
✔ In alcune situazioni può essere comodo avere a
disposizione una struttura dati che realizzi
contemporaneamente il comportamento di più tipi
di dati astratti
– ad esempio, una pila che metta a disposizione anche un
iteratore per ispezionare tutti i suoi elementi, come una
lista
– estendere una sola altra classe
• oppure implicitamente la classe Object
– realizzare più interfacce
✔ Se una classe realizza (“implementa”) più di una
interfaccia
– deve definire tutti i metodi di tutte le interfacce
– ha quindi un comportamento che è l’unione dei
comportamenti definiti dalle diverse interfacce
85
Strutture dati complesse
public class LinkedListStackList
implements Stack, List
{ private LinkedList list = new LinkedList();
public void makeEmpty()
// Container
{ list.makeEmpty(); }
public boolean isEmpty()
// Container
{ return list.isEmpty(); }
public void push(Object o) // Stack
{ list.addFirst(o); }
public void pop()
// Stack
{ list.removeFirst(); }
public Object top()
// Stack
{ return list.getFirst(); }
public Object topAndPop()
// Stack
{ return list.removeFirst(); }
public ListItr getIterator()// List
{ return list.getIterator(); }
}
86
Strutture dati complesse
87
LinkedListStackList t =
new LinkedListStackList();
Stack s = t;
s.push("X");
s.push("Y");
s.push("Z");
List list = t;
ListItr iter = list.getIterator();
while(iter.isInList())
{ System.out.println(iter.retrieve());
iter.advance();
}
s.makeEmpty();
iter.first();
while(iter.isInList())
{ System.out.println(iter.retrieve());
iter.advance();
}
Strutture dati complesse
Strutture dati complesse
✔ In questo modo tutto funziona
– i due assegnamenti ad interfaccia sono corretti perché la
classe LinkedListStackList realizza entrambe le
interfacce, quindi suoi esemplari possono essere visti
come una delle due interfacce
✔ Si nota un’incongruenza nell’utilizzo della
LinkedListStackList t = new LinkedListStackList();
Stack s = t;
List list = t;
✔ Si nota un’incongruenza nell’utilizzo della
struttura dati complessa
– l’esemplare della struttura viene assegnato ad un
riferimento del tipo della struttura stessa, e non ad un
riferimento ad interfaccia come si fa di solito
89
Marcello Dalpasso
Z
Y
X
88
struttura dati complessa
– l’esemplare della struttura viene assegnato ad un
riferimento del tipo della struttura stessa, e non ad un
riferimento ad interfaccia come si fa di solito
Stack t = new LinkedListStackList();
List list = (List) t;
✔ Questo funziona, ma non è molto esplicito
– chi legge il codice può (legittimamente) chiedersi come
sia possibile convertire un riferimento a Stack in un
riferimento a List
90
15
Fondamenti di Informatica 1
Settimana 9
Strutture dati complesse
Strutture dati complesse
✔ La cosa migliore sarebbe la definizione astratta del
✔ A questo punto, esiste il comportamento
comportamento dato dall’unione dei due
comportamenti, Stack e List
complesso definito da un’interfaccia
StackList t = new LinkedListStackList();
Stack s = t;
List list = t;
– successivamente, la classe LinkedListStackList si
definisce come realizzazione di tale comportamento
complesso
public interface StackList extends Stack, List
{ } // nessun metodo nuovo
✔ L’utilizzo della struttura è quello visto in
precedenza, ma ora è tutto più esplicito
public class LinkedListStackList
implements StackList
{ ... // tutto come prima
}
91
92
Strutture dati complesse
✔ Riassumendo
– una classe Java può
• estendere una sola altra classe
– oppure implicitamente la classe Object
• realizzare una o più interfacce
– un’interfaccia Java può
• estendere una o più altre interfacce
Intervallo
93
94
Albero binario in un array
✔ Abbiamo visto la realizzazione di un albero
Esercizio
Realizzazione di un albero in un array
binario (e di un ABR) come struttura costituita di
nodi concatenati
✔ Questo non è l’unico modo per realizzare l’ADT
“albero binario”
– un albero binario può essere realizzato usando un array
✔ Ricordiamo che un “albero binario” è un
contenitore di oggetti disposti in modo gerarchico,
dove ogni oggetto (tranne la radice) ha un genitore
e può avere al massimo due figli
95
Marcello Dalpasso
96
16
Fondamenti di Informatica 1
Settimana 9
Albero binario in un array
Albero binario in un array
✔ Per realizzare un albero in un array, dobbiamo
✔ Questa funzione di assegnamento degli indici
definire un modo per assegnare ad ogni nodo
dell’albero un numero intero da usare come indice
nell’array
– alla radice R assegniamo l’indice I(R) = 1
– se il nodo N è il figlio sinistro del nodo G
• allora I(N) = 2 · I(G)
– se il nodo N è il figlio destro del nodo G
• allora I(N) = 2 · I(G) + 1
97
Albero binario in un array
✔ Per realizzare l’array possiamo usare un array di
riferimenti ad Object che contenga le etichette dei
nodi dell’albero, in corrispondenza agli indici
✔ Se un nodo non esiste, la cella corrispondente
nell’array assume valore null
– questa realizzazione funziona solo per un albero con
etichette, con il vincolo che nessun nodo può avere
etichetta null
– per alberi senza etichette si usa un array di valori
booleani (true = nodo presente, false = nodo assente)
– per alberi con etichette eventualmente null bisogna
usare due array, uno di Object e uno di boolean
public interface BinaryTreeItr // non modificatore
{ // crea una copia dell’iteratore, copiando
// anche la posizione attuale
BinaryTreeItr clone();
// metodi di navigazione
// porta l’iteratore nella radice, come quando
// l’iteratore viene costruito;
// posizione non valida se l’albero e’ vuoto
void root();
// sposta l’iteratore di una posizione (può
// portarlo ad una posizione non valida)
void downLeft(); void downRight();
void up();
// metodi di ispezione
boolean isInTree(); // e’ in posizione valida?
boolean isInRoot(); // e’ nella radice?
boolean hasLeft(); // ha figlio sinistro?
boolean hasRight(); // ha figlio destro?
// restituisce l’oggetto (il dato) che si
// trova nella posizione attuale; restituisce
// null se l’iteratore non si trova in una
// posizione valida
Object retrieve();
}
Marcello Dalpasso
segue l’ordine di un attraversamento per livelli,
procedendo dall’alto in basso e da sinistra a destra
1
✔ Notiamo subito che non
tutti gli indici vengono
usati
– gli indici sono consecutivi
solo se l’albero è completo
2
4
3
5
✔ Notiamo anche che il primo indice è 1 e
non 0
– partendo da 0 le formule non funzionano
7
15
98
// etichette sempre presenti e non null
public class ArrayBinaryTree implements BinaryTree
{ private Object[] v = new Object[100];
private int count; // serve per isEmpty in O(1)
public ArrayBinaryTree()
{ makeEmpty(); }
public boolean isEmpty()
{ return count == 0; }
public void makeEmpty()
{ for (int i = 1; i < v.length; i++)
v[i] = null;
count = 0; } // la cella 0 non viene usata
public BinaryTreeItr getIterator()
{ return new ArrayBinaryTreeItr(); }
private class ArrayBinaryTreeItr
implements BinaryTreeItr
{ private ArrayBinaryTree tree;
public ArrayBinaryTreeItr(ArrayBinaryTree t)
{ tree = t; } ...
}
}
99
100
public class ArrayBinaryTree implements BinaryTree
{ ...
private class ArrayBinaryTreeItr
implements BinaryTreeItr
{ private ArrayBinaryTree tree;
int pos;
public ArrayBinaryTreeItr(ArrayBinaryTree t)
{ tree = t;
root(); }
public BinaryTreeItr clone()
{ ArrayBinaryTreeItr x =
new ArrayBinaryTreeItr(tree);
x.pos = pos;
return x; }
public void root()
{ pos = 1; }
public boolean isInRoot()
{ return pos == 1; }
...
}
}
101
102
17
Fondamenti di Informatica 1
public class ArrayBinaryTree implements BinaryTree
{ ...
private class ArrayBinaryTreeItr
implements BinaryTreeItr
{ private int left() { return pos * 2; }
private int right() { return 1 + pos * 2; }
private boolean isInTree(int p)
{ return p > 0 && p < tree.v.length
&& tree.v[p] != null; }
public boolean isInTree()
{ return isInTree(pos); }
public void downLeft() { pos = left(); }
public void downRight() { pos = right(); }
public void up() { pos = pos / 2; }
public boolean hasLeft()
{ return isInTree(left()); }
public boolean hasRight()
{ return isInTree(right()); }
public Object retrieve()
{ return isInTree() ? null : tree.v[pos]; }
}
}
Settimana 9
Attraversamenti
✔ Nell’attraversamento usiamo il metodo clone per
creare un nuovo iteratore, in modo da poter
navigare senza modificare la posizione attuale
103
public static void preOrderTraversal(BinaryTree t)
{ preOrderTraversal(t.getIterator());
}
private static void
preOrderTraversal(BinaryTreeItr it)
{ if (!it.isInTree()) return;
visit(it.retrieve());
BinaryTreeItr itLeft = it.clone();
itLeft.downLeft();
preOrderTraversal(itLeft);
BinaryTreeItr itRight = it.clone();
itRight.downRight();
preOrderTraversal(itRight);
}
104
Attraversamenti
✔ In alternativa, si può navigare usando up
public static void preOrderTraversal(BinaryTree t)
{ preOrderTraversal(t.getIterator());
}
private static void
preOrderTraversal(BinaryTreeItr it)
{ if (!it.isInTree()) return;
visit(it.retrieve());
if (it.hasLeft())
{ it.downLeft();
preOrderTraversal(it);
it.up(); }
if (it.hasRight())
{ it.downRight();
preOrderTraversal(it);
it.up(); }
}
Marcello Dalpasso
105
106
18