Fondamenti di Informatica 1
Settimana 8
Albero binario: ADT
✔ L’albero binario
– è un tipo di dati astratto
• è quindi definito da un’interfaccia
– è un contenitore
• ha quindi i metodi isEmpty e makeEmpty
– contiene dati organizzati in modo gerarchico
• diversamente dalla lista, che contiene dati
organizzati in modo sequenziale
– fornisce un iteratore per rappresentare la posizione al
suo interno
• l’iteratore non procede in modo sequenziale, ma
deve poter “navigare” nell’albero
Realizzazione di un albero
binario
2
1
Albero binario: ADT
Albero binario: realizzazione
✔ Realizziamo un albero binario prendendo lo
public interface BinaryTree extends Container
{ BinaryTreeItr getIterator();
}
spunto dalla definizione ricorsiva già vista
①L’albero vuoto è un albero binario
②Se X e Y sono alberi binari (eventualmente vuoti)
public interface BinaryTreeItr
{ ...
}
✔ Non vedremo i dettagli della realizzazione di un
senza nodi in comune e il nodo Z non appartiene
né ad X né a Y, allora inserendo la radice di X
come figlio sinistro di Z e la radice di Y come
figlio destro di Z si ottiene un albero binario con
radice Z
albero binario “professionale”
✔ Realizziamo un albero binario senza iteratore,
esponendo le sue variabili di stato
3
Albero binario: realizzazione
Albero binario vuoto
public class BinaryTree
{ public Object label; // etichetta della radice
public BinaryTree left, right;
public BinaryTree(Object e, BinaryTree l, BinaryTree r)
{ label = e; left = l; right = r; }
public BinaryTree(Object e)
{ this(e, null, null); }
}
✔ Per semplicità, usiamo una classe con lo stato
direttamente accessibile
✔ Non vengono rappresentati esplicitamente i nodi
✔ Non realizza l’interfaccia Container
✔ Questa classe non è in grado di rappresentare
l’albero binario vuoto (il nodo radice e la sua
etichetta esistono sempre)
– l’albero vuoto è un riferimento null
✔ In questo modo, ad esempio, l’albero avente la
sola radice avrà i campi left e right che valgono
null
– infatti, il sotto-albero sinistro e destro devono essere
alberi vuoti
✔ La rappresentazione è coerente
– perché non può essere vuoto!
5
Marcello Dalpasso
4
6
1
Fondamenti di Informatica 1
Settimana 8
Stato accessibile
Incapsulamento eccessivo?
✔ Le variabili di esemplare dell’albero possono
✔ A cosa serve l’incapsulamento in classi che hanno
lo stato completamente accessibile tramite metodi?
essere modificate senza invocare metodi
– apparentemente a niente…
BinaryTree t = new BinaryTree("X");
t.left = ...;
✔ Supponiamo di essere in fase di debugging e di
✔ Questo semplifica il codice, ma non sfrutta la
aver bisogno della visualizzazione di un
messaggio ogni volta che viene modificato il
valore di una variabile di un albero binario
potenza dell’incapsulamento
– si noti che l’incapsulamento è utile anche per classi
come questa, in cui lo stato sarebbe comunque
accessibile completamente con metodi set e get, come
avevamo visto per ListNode
7
– se non abbiamo usato l’incapsulamento, occorre
aggiungere enunciati in tutti i punti del codice dove
vengono usati gli alberi…
– elevata probabilità di errori o dimenticanze
Incapsulamento eccessivo?
Incapsulamento eccessivo?
✔ Se invece usiamo l’incapsulamento
– è sufficiente inserire l’enunciato di visualizzazione
all’interno dei metodi set che interessano
– le variabili di esemplare possono essere modificate
SOLTANTO mediante l’invocazione del
corrispondente metodo set
– terminato il debugging, per eliminare le visualizzazioni
è sufficiente modificare il solo metodo set, senza
modificare di nuovo moltissime linee di codice
✔ Usiamo un albero senza incapsulamento per
✔ Senza incapsulamento, si può scrivere
evidenziare il fatto che non è una soluzione
“professionale”
9
Albero binario: costruzione
A
– questo indurrà ricorsioni infinite nella maggior parte
degli algoritmi che operano sull’albero
✔ Usando, invece, l’incapsulamento
BinaryTree t = new BinaryTree("X");
t.setLeft(t);
si può definire il metodo setLeft in modo che
segnali un errore quando il parametro ricevuto è
uguale alla radice dell’albero
10
subtree
left
A
label
right
A
null
B
B
B
C
D
11
Marcello Dalpasso
violando le proprietà strutturali dell’albero binario
Albero binario
✔ Questo metodo di costruzione
viene detto top-down, cioè
dall’alto verso il basso
BinaryTree t = new BinaryTree("X");
t.left = t;
tree
BinaryTree tree = new BinaryTree("A");
BinaryTree subtree = new BinaryTree("B");
tree.left = subtree;
tree.left.left = new BinaryTree("C");
// oppure subtree.left = ...
tree.left.right = new BinaryTree("D");
// oppure subtree.right = ...
8
null
C
null
null
D
null
C
BinaryTree tree = new BinaryTree("A");
BinaryTree subtree = new BinaryTree("B");
tree.left = subtree;
subtree.left = new BinaryTree("C");
subtree.right = new BinaryTree("D");
D
12
2
Fondamenti di Informatica 1
Settimana 8
Albero binario: costruzione
✔ Supponiamo di aver già costruito un albero t e di
voler aggiungere un nuovo nodo, ad esempio una
nuova radice N, facendo in modo che l’attuale
albero ne diventi il sotto-albero destro
Intervallo
N
A
B
C
A
BinaryTree n = new BinaryTree("N");
n.right = t;
D
Questo metodo di costruzione
viene detto bottom-up, cioè
dal basso verso l’alto
B
C
D
13
14
Albero binario: dimensione
✔ La dimensione di un albero binario è il numero
dei suoi nodi
Algoritmi per alberi binari
public static int size(BinaryTree t)
{ if (t == null)
return 0;
return 1 + size(t.left) + size(t.right));
}
✔ Notiamo che è un algoritmo a ricorsione doppia
✔ Il caso base è quello di albero vuoto
✔ L’algoritmo è O(n), dove n è il numero di nodi
dell’albero, perché ogni nodo viene “visitato” una
ed una sola volta
16
15
Albero binario: altezza
Albero binario: altezza
✔ L’altezza di un albero binario è uguale all’altezza
del suo nodo radice
– non è definita se l’albero è vuoto (diciamo che vale -1)
✔ L’altezza di un nodo è uguale all’altezza maggiore
tra quelle dei suoi figli, aumentata di uno
✔ Controlliamo il caso base (che non è esplicito)
– una foglia ha il valore null sia in left sia in right
– le due invocazione di height restituiscono quindi -1
– il valore di Math.max(-1, -1) è, ovviamente, -1
– quindi 1 + Math.max(-1, -1) vale 0
✔ L’algoritmo è O(n), dove n è la dimensione
– l’altezza di una foglia è 0 (caso base)
– l’algoritmo è ricorsivo
public static int height(BinaryTree t)
{ if (t == null)
return -1; // non definita
return 1 + Math.max(height(t.left),
height(t.right));
}
Marcello Dalpasso
public static int height(BinaryTree t)
{ if (t == null)
return -1; // non definita
return 1 + Math.max(height(t.left),
height(t.right));
}
dell’albero
17
18
3
Fondamenti di Informatica 1
Settimana 8
Albero binario: copiatura
Attraversamento di un albero binario
✔ Vogliamo scrivere un metodo che riceve un albero
✔ Per scansione o attraversamento di un albero
binario si intende l’ispezione dei nodi dell’albero
in modo che
– tutti i nodi vengano ispezionati una ed una
sola volta
✔ Un attraversamento di un albero binario definisce
quindi un ordinamento completo tra i nodi
dell’albero
– in base alla loro posizione nell’albero
– NON in base alle loro etichette
binario e ne restituisce una copia identica
– è un algoritmo ricorsivo
– è O(n), dove n è la dimensione dell’albero
public static BinaryTree clone(BinaryTree t)
{ if (t == null)
return null;
return new BinaryTree(t.label, clone(t.left)
clone(t.right));
}
• che non sono necessariamente ordinabili
in modo che ogni nodo abbia un precedente e un
successivo all’interno dell’attraversamento
19
Attraversamento di un albero binario
Attraversamento di un albero binario
✔ Possibili attraversamenti
✔ In un albero binario si possono definire molte
–ABCD
–ABDC
–CDBA
– ...
✔ Ciascuna permutazione dei nodi
definisce un diverso
attraversamento
✔ Alcuni attraversamenti
procedono “a salti”… e sono
poco utili
A
politiche di attraversamento, ma ne esistono
alcune che hanno importanti applicazioni
– attraversamento in pre-ordine o in ordine
anticipato
B
C
A
D
B
C
• pre-order traversal A B C D
– attraversamento in post-ordine o in ordine
posticipato
• post-order traversal C D B A
D
– attraversamento in ordine simmetrico
• in-order traversal
CBDA
21
C
22
Attraversamento in ordine anticipato
L’azione di visita
✔ Per attraversare un albero binario in ordine
✔ L’attraversamento di un albero definisce soltanto
anticipato occorre
B
20
– visitare la radice
– attraversare ricorsivamente il sotto-albero sinistro
A della radice
– attraversare ricorsivamente il sotto-albero destro della
radice
D
public static void
preOrderTraversal(BinaryTree t)
if (t == null) return;
visit(t);
preOrderTraversal(t.left);
preOrderTraversal(t.right);
}
– non definisce quali “azioni” vengano compiute durante
la “visita”
– per effettuare la visita, viene semplicemente invocato il
generico metodo visit, che può essere definito come si
vuole
public static void visit(BinaryTree t)
{ System.out.println(t.label);
}
{
Marcello Dalpasso
l’ordine in cui vengono “visitati” i singoli nodi
dell’albero
23
24
4
Fondamenti di Informatica 1
Settimana 8
Attraversamento in ordine posticipato
Attraversamento in ordine simmetrico
✔ Per attraversare un albero binario in ordine
✔ Per attraversare un albero binario in ordine
posticipato occorre
B
C
simmetrico occorre
– attraversare ricorsivamente il sotto-albero sinistro
A della radice
– attraversare ricorsivamente il sotto-albero destro della
radice
– visitare la radice
D
public static void
postOrderTraversal(BinaryTree t)
{ if (t == null) return;
postOrderTraversal(t.left);
postOrderTraversal(t.right);
visit(t);
}
A
B
C
25
D
– attraversare ricorsivamente il sotto-albero sinistro
della radice
– visitare la radice
– attraversare ricorsivamente il sotto-albero destro della
radice
public static void
inOrderTraversal(BinaryTree t)
{ if (t == null) return;
inOrderTraversal(t.left);
visit(t);
inOrderTraversal(t.right);
}
26
Attraversamenti
✔ Questi tre algoritmi di attraversamento hanno una
complessità temporale asintotica O(n), dove n è la
dimensione dell’albero, cioè il numero dei suoi
nodi
✔ Infatti, ad ogni invocazione ricorsiva viene visitato
uno ed un solo nodo, e l’algoritmo garantisce che
tutti i nodi vengano visitati una ed una sola volta
27
28
Alberi di espressione
✔ Un albero binario è una struttura dati molto utile
per rappresentare un’espressione aritmetica
Gli alberi di espressione
(2+3)×(10-(4+1))
✔ Le foglie contengono
valori numerici
✔ I nodi interni
contengono operatori
artimetici (binari)
29
Marcello Dalpasso
×
+
2
3
+
10
4
1
30
5
Fondamenti di Informatica 1
Settimana 8
Alberi di espressione
Alberi di espressione
✔ Effettuando un attraversamento in post-ordine si
✔ Per ottenere la notazione infissa con le parentesi che la
ottiene l’espressione in notazione polacca inversa
2 3 + 10 4 1 + - ×
×
✔ L’attraversamento in
ordine simmetrico
fornisce invece la
notazione infissa
– mancano però le
(necessarie)
parentesi!
+
2
×
3
2 + 3 × 10 - 4 + 1
(2 + 3) × (10 - (4 + 1))
+
10
rendono corretta bisogna usare un algoritmo di
attraversamento leggermente modificato
– prima delle invocazioni ricorsive (quando “si scende”)
bisogna aprire una parentesi
– dopo le invocazioni ricorsive (quando “si sale”) bisogna
chiudere una parentesi
4
+
2
1
-
4
31
Alberi di espressione
+
3 10
1
32
Alberi di espressione
public static void printExpression(BinaryTree t)
{ if (t == null) return;
System.out.print("(");
printExpression(t.left);
visit(t);
printExpression(t.right);
System.out.print(")");
}
public static void visit(BinaryTree t)
{ System.out.print(t.label);
}
✔ Così facendo si ottiene, però, un’espressione con
parentesi attorno ad ogni singolo operando
((2) + (3)) × ((10) - ((4) + (1)))
33
public static void printExpression(BinaryTree t)
{ if (t == null) return;
if (t.left != null && t.right != null)
System.out.print("(");
printExpression(t.left);
visit(t);
printExpression(t.right);
if (t.left != null && t.right != null)
System.out.print(")");
}
✔ Osserviamo che in un albero di espressione non
esistono nodi con un solo figlio, quindi si può
semplificare la verifica
...
if (t.left != null) System.out.print("(");
...
if (t.left != null) System.out.print(")");
...
34
Alberi di espressione: valutazione
✔ Con un algoritmo basato su un attraversamento in ordine
posticipato è molto semplice valutare un’espressione
public static double evaluateExpression(BinaryTree t)
{ if (t == null) return 0; // espressione vuota
double left = evaluateExpression(t.left);
double right = evaluateExpression(t.right);
visit((String)t.label, left, right);
}
public static double visit(String op, double l, double r)
{ if (op == "+") return l + r;
if (op == "-") return l - r;
if (op == "*") return l * r;
if (op == "/") return l / r;
return Double.parseDouble(op); // una foglia
}
35
Marcello Dalpasso
Attraversamenti iterativi
36
6
Fondamenti di Informatica 1
Settimana 8
Attraversamento in ordine anticipato
Attraversamento in ordine anticipato
✔ Per realizzare un attraversamento in ordine
✔ Verifichiamo che effettivamente l’attraversamento
anticipato senza utilizzare la ricorsione, ci si deve
servire di una pila (come spesso accade…)
public static void preOrderTraversal(BinaryTree t)
{ Stack s = new ArrayStack();
if (t != null)
s.push(t);
while (!s.isEmpty())
{ BinaryTree c = (BinaryTree) s.topAndPop();
visit(c);
if (c.right != null)
s.push(c.right);
if (c.left != null)
s.push(c.left);
}
37
}
A
B
D
C
funzioni se l’albero e’ vuoto
– non effettua visite perché il ciclo termina subito
public static void preOrderTraversal(BinaryTree t)
{ Stack s = new ArrayStack();
if (t != null)
s.push(t);
while (!s.isEmpty())
{ BinaryTree c = (BinaryTree) s.topAndPop();
visit(c);
if (c.right != null)
s.push(c.right);
if (c.left != null)
s.push(c.left);
}
38
}
A
B
C
Attraversamento in ordine anticipato
Attraversamento in ordine anticipato
✔ Notiamo l’ordine in cui vengono effettuate le
✔ In questo caso, l’eliminazione della ricorsione è
invocazioni di push
– in questo modo si esamina prima il sotto-albero sinistro
public static void preOrderTraversal(BinaryTree t)
{ Stack s = new ArrayStack();
if (t != null)
s.push(t);
while (!s.isEmpty())
{ BinaryTree c = (BinaryTree) s.topAndPop();
visit(c);
if (c.right != null)
s.push(c.right);
if (c.left != null)
s.push(c.left);
}
39
}
A
B
C
D
Intervallo
stata (relativamente) semplice, perché l’algoritmo
aveva una ricorsione doppia, ma si trattava di
ricorsione in coda
public static void
preOrderTraversal(BinaryTree t)
{ if (t == null) return;
visit(t);
preOrderTraversal(t.left);
preOrderTraversal(t.right);
}
✔ Realizzare iterativamente gli altri attraversamenti
è meno banale
40
Ancora sulla notazione O-grande
41
Marcello Dalpasso
D
42
7
Fondamenti di Informatica 1
Settimana 8
Ancora sulla notazione O-grande
Ancora sulla notazione O-grande
✔ Abbiamo detto, in maniera poco “matematica”,
✔ Ovviamente, la caratterizzazione che interessa di
che una funzione è, ad esempio, O(n) se la sua
tendenza a crescere con n è quella di una funzione
lineare, O(n2) se è quadratica, e così via
✔ In realtà, la definizione matematica (che non
vediamo) prevede che
– se una funzione è O-grande di una certa funzione F(n),
allora è anche O-grande di qualsiasi funzione G(n) che
cresca più velocemente di F(n)
– cioè, se T(n) è O(F(n)) e G(n) ≥ F(n) (per elevati
valori di n), allora T(n) è anche O(G(n))
43
più è quella più precisa, cioè più stringente
✔ Però se, ad esempio
T(n) = O(n)
✔ non è sbagliato dire che
T(n) = O(n log n)
✔ oppure
T(n) = O(n2)
44
Il flusso di errore standard
✔ Abbiamo visto che un programma Java ha sempre
due flussi ad esso collegati
Il flusso di errore standard
– il flusso di ingresso standard, System.in
– il flusso di uscita standard, System.out
che vengono forniti dal sistema operativo
✔ In realtà esiste un altro flusso, chiamato flusso di
errore standard o standard error, rappresentato
dall’oggetto System.err
– System.err è di tipo PrintStream come System.out
46
45
Il flusso di errore standard
Il flusso di errore standard
✔ La differenza tra System.out e System.err è solo
✔ In condizioni normali (cioè senza redirezione) lo
convenzionale
– si usa System.out per comunicare all’utente i risultati
dell’elaborazione o qualunque altro messaggio che sia
previsto dal corretto e normale funzionamento del
programma
– si usa System.err per comunicare all’utente eventuali
condizioni di errore (fatali o non fatali) che si siano
verificate durante il funzionamento del programma
47
Marcello Dalpasso
standard error finisce sullo schermo insieme allo
standard output
✔ In genere il sistema operativo consente di
effettuare la redirezione dello standard error in
modo indipendente dallo standard output
– in Windows è possibile redirigere soltanto lo standard
output, mentre lo standard error rimane verso lo
schermo
– in Unix è possibile redirigere i due flussi verso due file
distinti
48
8
Fondamenti di Informatica 1
Settimana 8
Alberi binari di ricerca
49
Albero binario di ricerca
50
Albero binario di ricerca
① L’albero vuoto è un ABR
② Se
✔ L’albero binario di ricerca (ABR) è un
albero binario con alcune proprietà
aggiuntive
4
2
6
– ha etichette che sono dati appartenenti ad un
insieme in cui è definita una relazione d’ordine
completa
• le etichette sono riferimenti ad oggetti che
realizzano l’interfaccia Comparable
– soddisfa la definizione ricorsiva seguente
51
ABR: proprietà
è maggiore dell’etichetta
del suo figlio sinistro e
minore dell’etichetta del
suo figlio destro
– se tali figli esistono
52
✔ L’etichetta di ciascun nodo è
2
1
maggiore dell’etichetta del suo
figlio sinistro e minore
dell’etichetta del suo figlio destro
4
6
– se tali figli esistono
✔ Questo albero soddisfa la
8
3
7
proprietà, ma NON è un
ABR
9
53
Marcello Dalpasso
7 9
ABR: proprietà
✔ L’etichetta di ciascun nodo
✔ Ma attenzione!
– questa proprietà è
necessaria ma non
sufficiente
8
1 3
❶ X e Y sono ABR (eventualmente vuoti)
❷ tutte le etichette contenute in X sono minori di z
❸ tutte le etichette contenute in Y sono maggiori di z
allora inserendo
❶ l’etichetta z in un nuovo nodo Z
❷ la radice di X come figlio sinistro del nodo Z
❸ la radice di Y come figlio destro del nodo Z
si ottiene un ABR con radice Z
– perché 5 si trova nel sottoalbero DESTRO di 6 (e di 4)
1
4
2
6
8
10
5
9
54
9
Fondamenti di Informatica 1
Settimana 8
ABR: elementi duplicati
ABR: comportamento
✔ Dato che la definizione usa sempre la relazione
✔ L’albero binario di ricerca è un contenitore con il
“minore” e non “minore o uguale”
– l’albero binario di ricerca non può contenere elementi
duplicati
✔ Questo rende più semplice la realizzazione
dell’albero, ma non è un requisito teorico
– è possibile definire, realizzare e gestire ABR con
elementi duplicati
– non lo vediamo
55
compito fondamentale di rendere efficienti le
ricerche di dati in un insieme
✔ Le operazioni definite dalla sua interfaccia sono
– inserimento di un elemento
– rimozione di un elemento
– ricerca di un elemento
public interface BinarySearchTree
extends Container
{ void insert(Comparable obj)
throws DuplicateItemException;
void remove(Comparable obj)
throws ItemNotFoundException;
Comparable find(Comparable obj)
throws ItemNotFoundException;
}
Lancio di eccezioni
Lancio di eccezioni
void remove(...) throws ItemNotFoundException;
void remove(...) throws ItemNotFoundException;
✔ Abbiamo già visto che un metodo può lanciare
✔ La clausola throws deve essere presente nella
eccezioni a controllo non obbligatorio
– eccezioni che derivano da RuntimeException
✔ Se, invece, un metodo vuole lanciare eccezioni a
controllo obbligatorio
– l’eccezione deve essere derivata da Exception
– nella firma del metodo deve essere dichiarato
l’elenco delle possibili eccezioni a controllo
obbligatorio lanciate nel metodo
• si usa la clausola throws
57
ABR: realizzazione
void method(...) throws Exception1, Exception2;
✔ Attenzione: throws non lancia eccezioni, ma
dichiara che il metodo può lanciarle
– è una segnalazione al compilatore, che così
obbliga all’uso di un blocco try/catch
58
ABR: realizzazione
caratteristiche
– sarebbe naturale definire la classe BST come classe
derivata della classe BinaryTree e che realizza
l’interfaccia BinarySearchTree
public class BST extends BinaryTree
implements BinarySearchTree
{ … }
✔ Per semplicità, usiamo una classe con lo stato
direttamente accessibile
✔ Non lo facciamo, perché, come per l’albero
binario generico
Marcello Dalpasso
firma del metodo
– nella classe che realizza il metodo
– in eventuali interfacce che dichiarino il metodo
✔ Se il metodo può lanciare più eccezioni, vanno
elencate dopo throws, separandole con virgole
public class BST // BinarySearchTree
{ public Comparable label; // etichetta della radice
public BST left, right;
public BST(Comparable e, BST l, BST r)
{ label = e; left = l; right = r; }
public BST(Comparable e)
{ this(e, null, null); }
}
✔ L’ABR è un albero binario con particolare
– non vediamo una realizzazione “professionale”
dell’albero binario di ricerca
56
59
✔ Non vengono rappresentati esplicitamente i nodi
✔ Non realizza l’interfaccia Container
– perché non può essere vuoto!
– l’ABR vuoto si rappresenta con un riferimento null
60
10
Fondamenti di Informatica 1
Settimana 8
ABR: ricerca di un elemento
ABR: ricerca di un elemento
✔ Per cercare un elemento in un ABR si sfruttano le
sue proprietà peculiari, con un algoritmo ricorsivo
4
✔ Cerca l’elemento X nell’albero
2
– se l’albero è vuoto
6
8
7 9
1 3
• elemento non trovato
– se l’etichetta della radice è uguale a X
• elemento trovato
– altrimenti se l’etichetta della radice è minore di X
• cerca X nel sotto-albero destro
– altrimenti
• cerca X nel sotto-albero sinistro
public static Comparable find(Comparable x, BST t)
throws ItemNotFoundException
{ if (t == null)
throw new ItemNotFoundException();
if (t.label.compareTo(x) == 0)
return t.label;
if (t.label.compareTo(x) < 0)
return find(x, t.right);
return find(x, t.left);
}
✔ Notiamo che l’algoritmo usa la
ricorsione in coda
– quindi è semplice scriverne 2
una versione iterativa
1 3
4
6
8
7 9
61
62
ABR: ricerca di un elemento
public static Comparable find(Comparable x, BST t)
throws ItemNotFoundException
{ while (t != null)
{ if (t.label.compareTo(x) == 0)
return t.label;
if (t.label.compareTo(x) < 0)
t = t.right;
else t = t.left;
}
throw new ItemNotFoundException();
}
Intervallo
4
2
1 3
6
8
7 9
63
ABR: ricerca di un elemento
ABR: ricerca di un elemento
public static Comparable find(Comparable x, BST t)
throws ItemNotFoundException
{ while (t != null)
{ if (t.label.compareTo(x) == 0)
return t.label;
if (t.label.compareTo(x) < 0)
t = t.right;
else t = t.left;
}
throw new ItemNotFoundException();
}
4
2
6
8
1 3
7 9
✔ Perché viene restituito un riferimento al dato
trovato e non semplicemente un valore booleano?
– si potrebbe pensare che il riferimento restituito, in caso
di successo, sia sempre uguale al riferimento x
65
Marcello Dalpasso
64
✔ La ricerca in un ABR non ha come scopo
l’identificazione di un riferimento identico al
riferimento ricevuto…
– ha lo scopo di trovare un oggetto che superi
positivamente il confronto con l’oggetto a cui si
riferisce il riferimento ricevuto
– i due oggetti possono quindi essere diversi
• anche “molto diversi”
• dipende da come è stato definito il metodo
compareTo per tali oggetti
66
11
Fondamenti di Informatica 1
Settimana 8
ABR: ricerca di un elemento
Ricerca di valori per chiave
✔ In generale, il metodo compareTo usa una parte
✔ Questa modalità di ricerca viene detta
– ricerca di valori per chiave
✔ La chiave è una parte del valore con la seguente
dello stato degli oggetti per fare il confronto
– ad esempio, per confrontare due conti bancari potrebbe
essere ragionevole confrontare soltanto il numero di
conto e non il saldo
proprietà
– identifica in modo univoco i valori all’interno
dell’insieme
• il numero di conto è una proprietà di stato degli oggetti di
tipo “conto bancario” che identifica univocamente oggetti
distinti
• il saldo non ha tale proprietà (conti diversi con saldo uguale)
✔ Dal punto di vista della programmazione a oggetti,
– una proprietà di stato di un insieme di oggetti che abbia
la caratteristica di identificare univocamente gli oggetti
distinti nell’insieme si chiama chiave (key)
67
la coppia chiave/valore può essere rappresentata
da una coppia di oggetti
– come vedremo per un dizionario
oppure da un unico oggetto
– come vediamo in un ABR (l’etichetta)
68
ABR: complessità della ricerca
ABR: complessità della ricerca
✔ Quanti passi (ricorsivi o iterativi) sono necessari
✔ Indichiamo con h l’altezza dell’albero
4
per cercare un elemento in un ABR?
– si parte dalla radice
– ad ogni passo si scende di un livello
– ciascun passo viene eseguito in O(1)
2
6
8
1 3
✔ Evidentemente il tempo di ricerca dipende dalla
7 9
posizione in cui si trova l’elemento
– il numero massimo di passi è uguale all’altezza
dell’albero
– il numero medio di passi è proporzionale all’altezza
dell’albero (si può dimostrare)
– la complessità di caso peggiore è quindi O(h)
– la complessità media è anch’essa O(h)
✔ Se vogliamo valutare la complessità in funzione di
n, la dimensione dell’albero, bisogna trovare una
relazione tra h ed n
4
✔ Se l’albero è completo
h = log2 (n+1) - 1
quindi, per grandi valori di n
h = O(log n)
2
1 3
6
8
7 9
69
70
ABR: complessità della ricerca
ABR: complessità della ricerca
✔ In generale, però, l’ABR non sarà completo
✔ Consideriamo un ABR “massimamente
sbilanciato”, o degenere
✔ Se l’albero è bilanciato, si può dimostrare che
– l’albero è, in realtà, una catena
– l’altezza h di un albero binario bilanciato di
dimensione n è O(log n)
✔ Le prestazioni di caso medio e di caso peggiore
della ricerca in un ABR bilanciato sono quindi
4
O(h) = O(log n)
2
6
✔ E se l’ABR non è bilanciato?
1 3
Marcello Dalpasso
8
7 9
4
6
8
✔ In questo caso l’altezza dell’albero è uguale
71
9
h = n-1 = O(n)
quindi anche la complessità asintotica della ricerca
diventa O(n), come in una catena con iteratore
✔ Riprenderemo questo discorso dopo aver visto
come si effettuano inserimenti e rimozioni
nell’ABR
72
12
Fondamenti di Informatica 1
Settimana 8
ABR: inserimento di un elemento
ABR: inserimento di un elemento
✔ Per inserire un elemento in un ABR si sfrutta
✔ In quale posizione va inserito il nuovo nodo
dell’ABR che dovrà contenere il nuovo elemento?
l’algoritmo di ricerca...
– va inserito nella posizione in cui lo si sarebbe trovato
con l’algoritmo di ricerca se fosse stato già presente
nell’albero
✔ Dato che l’inserimento è possibile soltanto se
l’elemento da inserire NON si trova già nell’ABR,
si effettua prima di tutto una ricerca
4
2
1 3
✔ La ricerca fallisce quando si cerca in un albero
– se la ricerca ha successo, l’inserimento fallisce
4
– altrimenti, in quale posizione va inserito il nuovo nodo
6 dell’ABR che dovrà contenere il nuovo elemento?
8
7 9
2
1
vuoto
– si inserisce il nuovo nodo come radice di tale albero
vuoto e lo si collega al genitore
6
8
3
7 9
73
74
ABR: inserimento di un elemento
ABR: inserimento di un elemento
✔ Non vediamo la realizzazione in Java
– la rappresentazione di un ABR con un oggetto della
classe BST è troppo semplificata
✔ Quali sono le prestazioni dell’algoritmo di
inserimento in un ABR?
– dopo aver effettuato una ricerca, si fanno soltanto
operazioni che richiedono un tempo costante
public static void insert(Comparable x, BST t)
throws DuplicateItemException
{ ...
}
– le prestazioni dell’inserimento sono identiche a quelle
della ricerca
4
– per inserire un nodo in un albero vuoto (cioè se t vale
null) dobbiamo creare un nuovo albero
• ma come facciamo a restituirlo al chiamante?
• se modifichiamo il valore di t non si ha alcun effetto
• t non punta ad un oggetto che possiamo modificare
– si dovrà usare un elemento header come nella catena
• in modo che ci sia sempre un nodo
75
2
1 3
6
8
7 9
ABR: inserimento di un elemento
ABR: inserimento di un elemento
✔ Sappiamo che le prestazioni dell’ABR dipendono
✔ L’algoritmo di inserimento che abbiamo visto
dalle sue caratteristiche topologiche
garantisce la generazione di un ABR bilanciato?
– è bilanciato o no?
– la risposta è negativa
✔ Le caratteristiche topologiche dipendono dalle
✔ Le caratteristiche topologiche dell’ABR costruito
modalità di costruzione dell’ABR
– l’ABR viene costruito con l’algoritmo di inserimento
(eventualmente usando anche rimozioni) a partire da un
ABR vuoto
con l’algoritmo di inserimento che abbiamo visto
dipendono dall’ordine in cui vengono inseriti gli
elementi nell’albero stesso!
4
6
✔ L’algoritmo di inserimento che abbiamo visto
garantisce la generazione di un ABR bilanciato?
77
Marcello Dalpasso
76
8
– ad esempio, se i dati vengono inseriti in ordine
crescente, si ottiene un albero completamente
sbilanciato a destra
9
78
13
Fondamenti di Informatica 1
Settimana 8
ABR: inserimento di un elemento
ABR: bilanciamento
✔ Ci chiediamo
– quali sono le caratteristiche topologiche dell’ABR se i
dati vengono inseriti in ordine casuale?
✔ Abbiamo visto quanto sia importante, per le
prestazioni dell’ABR, fare in modo che l’albero
sia sempre bilanciato dopo ogni inserimento
✔ L’argomento va al di là degli obiettivi del corso,
però è importante sapere che
– esistono algoritmi di inserimento che
mantengono un ABR sempre bilanciato,
indipendentemente dall’ordine di inserimento
dei dati (es. alberi AVL, alberi rosso-nero…)
– tali algoritmi garantiscono prestazioni O(log n)
per l’inserimento e la ricerca in un ABR
✔ Si può dimostrare che l’ABR così ottenuto ha,
mediamente, altezza proporzionale al logaritmo
della sua dimensione
✔ La ricerca e l’inserimento in un ABR hanno
quindi prestazioni
– O(log n) nel caso medio
– O(n) nel caso peggiore
• O(log n) anche nel caso peggiore, se l’ABR è bilanciato
79
80
Alberi binari di ricerca
81
82
ABR: attraversamento in-order
ABR: attraversamento in-order
✔ L’attraversamento in ordine
simmetrico di un ABR visita i
nodi in una sequenza molto
particolare
– in ordine crescente
✔ Gli altri attraversamenti non sono
4
2
1
6
8
3
7
9
altrettanto interessanti
83
Marcello Dalpasso
✔ Applicazione
– per ordinare un insieme di n dati, è possibile costruire un
ABR che li contiene e poi effettuare un attraversamento in
ordine simmetrico per ottenerli in ordine crescente
– quali sono le prestazioni di questo ordinamento?
• se l’ABR è sempre bilanciato, l’inserimento di un
elemento ha un costo O(log k), dove k è la dimensione
dell’albero
– k aumenta ogni volta…
• l’inserimento di tutti gli elementi ha un costo O(n log n)
– nell’ipotesi che l’ABR sia sempre bilanciato
• l’attraversamento ha un costo O(n), come per ogni albero
– l’algoritmo di ordinamento è quindi O(n log n)
• nell’ipotesi che l’ABR sia sempre bilanciato
84
14
Fondamenti di Informatica 1
Settimana 8
ABR: ricerca del minimo
ABR: ricerca del minimo
✔ La ricerca dell’elemento minimo
✔ Per la ricerca del massimo si
in un insieme di dati è un
problema piuttosto frequente
✔ In un ABR l’algoritmo di ricerca
dell’elemento minimo è molto
semplice ed efficiente
2
procede verso destra finché è
possibile
1 3
✔ Osserviamo che i nodi che
contengono l’elemento minimo o
massimo sono
4
– inoltre la soluzione di questo
problema ci servirà in seguito
2
1
6
8
3
7
– partendo dalla radice, si procede
verso sinistra finché è possibile
– il nodo in cui ci si ferma (che non ha
figlio sinistro) contiene l’etichetta di
valore minimo all’interno dell’albero
8
7
9
✔ La ricerca del
– hanno un solo figlio
✔ In caso contrario, l’algoritmo
avrebbe potuto procedere oltre
minimo/massimo
è O(log n)
85
86
ABR: rimozione di un elemento
✔ Dopo aver trovato l’elemento da rimuovere, si
✔ Per rimuovere un elemento da un ABR si sfrutta
presentano tre casi che richiedono varianti
diverse dell’algoritmo
ancora l’algoritmo di ricerca...
✔ Dato che la rimozione è possibile soltanto se
– il nodo da rimuovere è una foglia
– il nodo da rimuovere ha un solo figlio
– il nodo da rimuovere ha due figli
l’elemento da inserire si trova nell’ABR, si
effettua prima di tutto una ricerca
– se la ricerca non ha successo, la rimozione fallisce
87
✔ Eliminare una foglia è molto semplice
– si inserisce il valore null nel nodo genitore al posto
6
del riferimento che punta al nodo da eliminare
8 (figlio destro o figlio sinistro del genitore)
7 9 • tutte le proprietà dell’albero sono conservate 88
ABR: rimozione di un elemento
ABR: rimozione di un elemento
✔ Per rimuovere un nodo X che ha un solo figlio Y
– si connette il figlio Y di X come figlio del genitore Z di X
• se X era il figlio sinistro (destro) di Z, Y diventa il
figlio sinistro (destro) di Z
• Z<XeX<Y⇒Z<Y
• tutti i discendenti di Y sono maggiori di X e X > Z
⇒ tutti i discendenti di Y sono maggiori di Z
✔ Per rimuovere un nodo X che ha due figli Y e Z
4
1 3
6
– foglie
oppure
9
ABR: rimozione di un elemento
2
4
– altrimenti, al termine della ricerca conosciamo già la
6 posizione dell’elemento da rimuovere
8
7 9
4Z
2
6 X
8Y
1 3
7 9
Marcello Dalpasso
4
2
1 3
4 Z
2
8Y
1 3 7 9
89
– si cerca il minimo W nel sotto-albero destro di X
– si copia l’etichetta di W nell’etichetta di X
– si elimina il nodo W
• operazione che sappiamo perché W è una
foglia oppure ha un solo figlio
4X
Y 2
7 Z
1 3 6 8
W 9
6W
7 Z
Y2
8
1 3
9
90
15
Fondamenti di Informatica 1
Settimana 8
ABR: rimozione di un elemento
4X
Y 2
7 Z
6
8
3
1
W 9
6W
7 Z
Y2
3
8
1
9
ABR: rimozione di un elemento
✔ Le prestazioni dell’algoritmo di rimozione in un
ABR bilanciato sono O(log n)
– si fa una ricerca, che è O(log n)
– si fanno alcune operazioni che sono O(1)
• se il nodo da eliminare ha due figli, si fa
anche la ricerca dell’elemento minimo, che è
comunque O(log n)
✔ Tutti i discendenti di Z sono maggiori di X
⇒W > X
✔W>XeX>Y
⇒W > Y
✔ W è l’elemento minimo del sotto-albero avente
radice Z
⇒tutti i rimanenti elementi del sotto-albero avente
radice Z (che vanno a formare il sotto-albero destro
91
di W) sono maggiori di W
92
Intervallo
Dizionario
93
94
Dizionario
Dizionario
✔ Un dizionario è un ADT con le seguenti proprietà
– è un contenitore
– consente l’inserimento di coppie di dati di tipo
• chiave / valore
con la chiave che deve essere unica nell’insieme dei
dati memorizzati
• non possono esistere nel dizionario due valori con
identica chiave
– consente di effettuare in modo efficiente la ricerca e la
rimozione di valori usando la chiave come
identificatore
95
✔ L’analogia con il dizionario di uso comune è
Marcello Dalpasso
molto forte
✔ In un comune dizionario
– le chiavi sono le singole parole
– il valore corrispondente ad una chiave è la definizione
della parola nel dizionario
– tutte le chiavi sono distinte
– ad ogni chiave è associato uno ed un solo valore
– la ricerca di un valore avviene tramite la sua chiave
96
16
Fondamenti di Informatica 1
Settimana 8
Dizionario
Dizionario con un ABR
public interface Dictionary extends Container
{ // l’inserimento va sempre a buon fine;
// se la chiave non esiste, la coppia
// key/value viene aggiunta al dizionario;
// se la chiave esiste già, il valore ad
// essa associato viene sovrascritto con
// il nuovo valore
void insert(Comparable key, Object value);
// la rimozione della chiave rimuove anche
// il corrispondente valore
void remove(Comparable key)
throws ItemNotFoundException;
// la ricerca per chiave restituisce soltanto
// il valore ad essa associato
Object find(Comparable key)
throws ItemNotFoundException;
}
97
Dizionario con un ABR
✔ Per realizzare un dizionario possiamo usare un
ABR
public interface BinarySearchTree
extends Container
{ void insert(Comparable obj)
throws DuplicateItemException;
void remove(Comparable obj)
throws ItemNotFoundException;
Comparable find(Comparable obj)
throws ItemNotFoundException;
}
✔ Supponiamo di avere a disposizione una classe
BST che realizzi veramente l’interfaccia
BinarySearchTree
– non quella che abbiamo visto noi, che è solo un
prototipo
98
Dizionario con un ABR
✔ L’ABR è però dotato di etichette che possono
contenere un Comparable, ma non una coppia di
dati Comparable/Object
✔ Dobbiamo creare un oggetto Pair che contenga la
coppia chiave/valore ed usarlo come etichetta
✔ Tali oggetti devono essere Comparable
public class Pair implements Comparable
{ public Comparable key;
public Object value;
public Pair(Comparable k, Object v)
{ key = k; value = v; }
public int compareTo(Object x)
{ return key.compareTo(((Pair)x).key);
}
}
99
Dizionario con un ABR
public class BSTDictionary implements Dictionary
{ ...
public void remove(Comparable key)
throws ItemNotFoundException
{ bst.remove(new Pair(key, null)); }
}
✔ Per rimuovere una coppia dal dizionario
– si crea una coppia “fittizia” che contiene la chiave da
cercare e si rimuove tale coppia dall’albero
– dato che i confronti nell’albero avvengono usando il
metodo compareTo della classe Pair, che controlla
soltanto il campo chiave, viene effettivamente eliminata
la coppia che contiene tale chiave
• se non esiste viene lanciata un’eccezione, che non
essendo gestita dal metodo si propaga
101
(correttamente) al chiamante
Marcello Dalpasso
public class BSTDictionary implements Dictionary
{ private BinarySearchTree bst = new BST();
public boolean isEmpty()
{ return bst.isEmpty();
}
public void makeEmpty()
{ bst.makeEmpty();
} ...
}
100
Dizionario con un ABR
public class BSTDictionary implements Dictionary
{ ...
public void insert(Comparable key, Object value)
{ try { bst.insert(new Pair(key, value)); }
catch (DuplicateItemException e)
{ Pair p = (Pair) bst.find(new Pair(key, null));
p.value = value; } }
...
}
✔ Per inserire una coppia nel dizionario, si crea la
coppia e si prova ad inserirla nell’albero
– se l’inserimento nell’ABR fallisce, significa che esiste
già una coppia con la stessa chiave, quindi la si cerca
(non si può fallire) e si modifica il suo valore
102
17
Fondamenti di Informatica 1
Settimana 8
Dizionario con un ABR
Dizionario con ABR: prestazioni
public class BSTDictionary implements Dictionary
{ ...
public Object find(Comparable key)
throws ItemNotFoundException
{ Pair p = (Pair) bst.find(new Pair(key, null));
return p.value; }
}
✔ Per cercare un valore nel dizionario tramite la sua
chiave
– si crea una coppia “fittizia” che contiene la chiave da
cercare e si cerca tale coppia dall’albero
– se non esiste, viene lanciata e propagata un’eccezione
– se esiste, si riceve un riferimento ad una coppia
• tale coppia contiene il valore da restituire
103
✔ Tutti i metodi dell’ABR utilizzati dalla
realizzazione del dizionario sono O(log n)
– nell’ipotesi che l’ABR sia bilanciato
✔ Nelle realizzazioni dei metodi non sono presenti
cicli, né ricorsioni
✔ Quindi, tutti i metodi di questa realizzazione del
dizionario sono O(log n), dove n è il numero di
coppie chiave/valore presenti nel dizionario
– nell’ipotesi che l’ABR sia bilanciato
104
105
Marcello Dalpasso
18