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