Dipartimento di Elettronica, Informazione e Bioingegneria API 2013/4 Alberi @ G. Gini 2013 Alberi • Alberi binari Rappresentazioni Proprietà • Alberi n-ari • attraversamento • Alberi binari di ricerca Bilanciamento G. Gini 2013/4 Famiglia - 1700 circa G. Gini 2013/4 The art of computer programming • Donald Knuth, 1968-1973 • Enciclopedia dell’informatica in 7 volumi • TeX "Beware of bugs in the above code; I have only proved it correct, not tried it." G. Gini 2013/4 albero L’albero e’ un tipo astratto di dati usato per rappresentare relazioni gerarchiche. - struttura del file system - -albero genealogico - organigramma - albero di decisione - …. G. Gini 2013/4 Albero sintattico Input: s=6*8+((2+42)*(5+12)+987*7*123+15*54) Output: exp term fact term fact fact exp term 6 * 8 + ( ( term fact fact exp exp term term term term fact fact fact fact 5 + 12 2 + 42 ) * ( ) fact fact + 978 * 7 Osservazione: vale anche per il linguaggio naturale G. Gini 2013/4 term fact fact * 123 + 15 fact * 54 ) parsing di espressioni notazione algebrica normale (INORDER) (9+7)*(3-(4+1)) notazione polacca inversa - postfissa(POSTORDER) 97+341+-* G. Gini 2013/4 Albero binario – def ricorsiva Un albero è vuoto oppure è un nodo con due figli, sinistro e destro, che sono a loro volta alberi binari. <albero-binario> ::= NIL | <nodo> <albero-binario><albero-binario> Rappresentazione grafica: ogni nodo dell'albero contiene un elemento dell'insieme e due frecce, a figlio sinistro e a figlio destro. B A D C G. Gini 2013/4 primitive sugli alberi binari • • • • • • 1. vuoto? (cioè NULL) 2. radice 3. sinistro 4. destro 5. aggiungi-nodo 6. rimuovi-nodo Notazioni Nodo, arco Figlio, discendente, genitore,.. Radice, foglia, ramo G. Gini 2013/4 Es: contare le foglie contafoglie(x) - per x vuoto è zero, altrimenti è la somma delle foglie del sottoalbero sinistro e del sottoalbero destro. -------------------------------------int contafoglie ( tree-item x) --------------------------------------if vuoto? (x) then return 0 if foglia(x) then return 1 else return (contafoglie (sinistro(x)) + (contafoglie (destro(x)) Funzione ausiliaria -------------------------------------boolean foglia ( tree-item x) --------------------------------------if vuoto? (x) then return false if vuoto?(sinistro(x)) AND vuoto?(destro(x)) then return true G. Gini 2013/4 Es: conta foglie (in C) int conta-f ( tree t ) { if ( t == NULL ) /*se vuoto*/ return 0; if ( t->left == NULL && t->right == NULL); /*se foglia*/ return 1; return conta-f( t->right ) + conta-f(t->left); } G. Gini 2013/4 Profondità e altezza Profondità = il numero di livelli dell’albero A ha profondità 0. B e C sono a livello 1. Altezza = massima profondità + 1 L’albero ha altezza 4. 0 1 2 3 4 altezza G. Gini 2013/4 Alcune definizioni • Albero binario pieno (Full binary tree) - ogni nodo è una foglia oppure un nodo interno con 2 figli non vuoti • Albero binario completo (Complete binary tree) - se l’altezza dell’albero è d, allora tutti i nodi tranne eventualmente quelli al livello d sono full. G. Gini 2013/4 Full Binary Tree TEOREMA: il numero di foglie in un full binary tree non vuoto è il numero dei nodi interni +1 • Dimostrazione (per induzione matematica): • Caso base: un full binary tree con 1 nodo interno ha due foglie • Ipotesi induttiva: assumiamo che un full binary tree T con n-1 nodi interni ha n foglie Passo induttivo: dato l’albero full binary tree T con n nodi interni, considera il nodo interno I con due figli foglia. Togli i figli di I e ottieni l’albero T’. • Per ipotesi induttiva, T’ è un full binary tree con n foglie. Riaggiungi i figli di I. Il numero dei nodi interni ora aumenta di 1 e diventa n. Il numero di foglie pure esso aumenta di 1. G. Gini 2013/4 Rappresentazioni per alberi Anche per gli alberi come per le liste si possono avere 2 rappresentazioni: 1. mediante array 2. a puntatori Per rappresentare un albero binario di profondità n basta un array in cui riservare memoria per ogni nodo; nel caso di alberi sbilanciati i nodi mancanti avranno un valore di default (-1). 7 /\ 3 9 /\ \ 1 5 11 7 G. Gini 2013/4 3 9 1 5 -1 11 Dimensionare l’array 1D profondità = il numero di livelli dell’albero, ovvero il numero di archi da attraversare per andare dalla radice alla foglia più lontana (nell’esempio 2) 7 / \ 3 9 / \ \ 1 5 11 Un albero binario pieno (full) di profondità n ha: 1 nodo radice + 2 nodi primo livello + 4 nodi di secondo livello +... Quindi il numero dei nodi è: 20+21+…+2n = n Σ2 i i=0 G. Gini 2013/4 Implementazione in array 2D posizione 0 1 2 3 4 5 6 7 8 9 10 11 genitore -- 0 0 1 1 2 2 3 3 4 4 5 Figlio sinistro 1 3 5 7 9 11 -- -- -- -- -- -- Figlio destro 2 4 6 8 10 -- -- -- -- -- -- -- Fratello sinistro -- -- 1 -- 3 -- 5 -- 7 -- 9 -- Fratello destro -- 2 -- 4 -- 6 -- 8 -- 10 -- -- G. Gini 2013/4 Rappresentazione a puntatori Vediamo l’albero binario come una lista di scatole di tre elementi: l’informazione, il puntatore al sottoalbero sinistro, il puntatore al sottoalbero destro. typedef struct Nodo { Tipo data; struct Nodo *left; struct Nodo *right; } nodo; B A D typedef nodo * tree; C G. Gini 2013/4 nota • Le foglie sono implementate come i nodi interni, quindi si spreca spazio per i puntatori nulli. G. Gini 2013/4 Ricerca elemento Restituisce NULL se non trova nell’albero t il dato d, altrimenti restituisce il puntatore al primo nodo che lo contiene visita t in preordine sinistro --------------------------------tree trova ( tree t, d) -------------------------------if null(t) then return NULL if ( t->dato = d ) then return t; /*trovato*/ return (trova( t->left, d) || trova( t->right, d )) G. Gini 2013/4 ricerca con assegnamenti (in C) tree trova ( tree t, int d) { tree temp; if ( t == NULL) return NULL; if ( t->dato == d ) return t; temp = trova( t->left, d ); if ( temp == NULL ) return trova( t->right, d ); else return temp; } G. Gini 2013/4 Es: calcolo della altezza int height ( tree t ) { if (t == NULL) return 0; return max ( heigth( t->left), heigth( t>right )) + 1; } int max ( int a, int b ) { if (a >b) return a; else return b; } G. Gini 2013/4 Es: conteggio dei nodi - in C int contaNodi ( tree t ) { if ( t == NULL ) return 0; else return (contaNodi(t->left) + contaNodi(t->right) + 1); /* c’è anche il nodo corrente */ } G. Gini 2013/4 Es: conta i nodi non-foglia int interni ( tree t ) { if ( t == NULL ) return 0; if ( t->left == NULL && t->right == NULL)) return 0; return 1 + interni( t->right ) + interni(t->left); } oppure… int interni2 ( tree t ) { return contaNodi( t ) – conta-f( t ); } Si noti che la seconda versione scandisce due volte l'intero albero, impiegando circa il doppio del tempo (e lo stesso spazio in memoria) G. Gini 2013/4 Es: numero di nodi su livelli pari Consideriamo che il livello della radice, cioè 0, sia pari . Un albero col solo nodo radice ha 1 livello. int evenCount ( tree t ) { if ( t == NULL ) return 0; if ( t->left == NULL ) && (t->right==NULL) return 1; return evenCount( t->left->left) + evenCount( t->left->right ) + evenCount( t->right->left ) + evenCount( t->right->right ) + 1; } G. Gini 2013/4 … oppure con assegnamenti int evenCount ( tree t ) { int n = 1; /* Il nodo corrente è sempre su livello pari, per ipotesi induttiva*/ if ( t == NULL ) return 0; if ( t->left != NULL ) /* ci sono nodi a sinistra*/ n += evenCount( t->left->left ) + evenCount( t->left>right ); if ( t->right != NULL ) */ ci sono nodi a destra*/ n += evenCount( t->right->left ) + evenCount( t->right>right ); return n; } G. Gini 2013/4 Es: numero di nodi su livelli dispari int oddCount ( tree t, int level ) { int n; if ( t == NULL ) return 0; n = oddCount( t->left, level+1 ) + oddCount( t->right, level+1 ); if ( level%2 == 1 ) /* == 0 Æ per evenCount */ n++; return n; } oppure oddCount (t) = contanodi (t) – evenCount(t) G. Gini 2013/4 … numero di nodi su livelli dispari Si può vedere come il calcolo del numero dei nodi di livello pari partendo dal livello 1 invece che 0 int oddCount ( tree t) { if ( t == NULL ) return 0; /* vuoto*/ return evenCount ( t->left ) + evenCount (t->right) } G. Gini 2013/4 Albero n-ario ogni nodo ha un numero arbitrario di figli • Si usa ad esempio per rappresentare tassonomie e organizzazioni gerarchiche definito ricorsivamente: <albero> =ε | nodo seguito da un numero finito di alberi G. Gini 2013/4 Conversione in albero binario • • Il figlio di sinistra diventa sottoalbero sinistro; il sottoalbero destro contiene il primo fratello verso destra G. Gini 2013/4 Implementazioni sequenziali ad hoc Lista semplice con i nodi nell’ordine di visita (ad esempio pre-order) • • • • Risparmio di spazio ma solo accesso sequenziale Occorre conservare la struttura dell’albero per poterlo ricostruire esempi: se l’albero è full, marca i nodi come foglie o come interni A’B’/DC’E’G/F’HI usa un simbolo per marcare i NULL AB/D//CEG///FH//I// G. Gini 2013/4 Rappresentare solo foglie e struttura • • le liste in LISP sono alberi con l’informazione (i simboli atomici) memorizzata nelle foglie. la lista (a (b c) d (e f g)) corrisponde all’albero seguente • In LISP è memorizzato come albero binario G. Gini 2013/4 con diverso numero di puntatori Ingestibile G. Gini 2013/4 Rappresentazione con 2 puntatori A B D E F G Il nodo è una struttura con 3 campi: 1. informazione, 2. puntatore al primo figlio, C 3. puntatore al fratello a destra H I A null Sottoalbero B B G. Gini 2013/4 Sottoalbero C C null Realizzazione con 3 puntatori / / / / / / / / / / / / / / / / Padre Nodo G. Gini 2013/4 Primo Figlio Fratello Attraversamento di albero • Un processo per visitare i nodi di un albero in un dato ordine è detto attraversamento (tree traversal) • Ogni attraversamento è una enumerazione dei nodi dell’albero G. Gini 2013/4 Algoritmi di visita degli alberi per “visitare” tutti i nodi di un albero ✦ In profondità (depth-first search, a scandaglio, DFS) ✦ Vengono visitati i rami, uno dopo l’altro ✦ Tre varianti in, pre, post-ordine In ampiezza (breadth-first search, a ventaglio, BFS) ✦A livelli, partendo dalla radice ✦ G. Gini 2013/4 Modi di visita – albero binario • Pre-ordine, o pre-visita, o anticipato: visita ogni nodo prima di visitare i suoi figli • Post-ordine, o post-visita, o posticipato: visita ogni nodo dopo aver visitato i suoi figli • In-ordine, o simmetrico: visita il sottoalbero di sinistra, quindi il nodo, poi il sottoalbero di destra Si estende per albero n-ario G. Gini 2013/4 Esempi visita df Previsita Postvisita Simmetrica G. Gini 2013/4 df - ordine anticipato - preorder T a b c e d f g a b c d e f g void preorder (tree rt) { if (rt == NULL) return; // Empty visit(rt); preorder(rt->left); preorder(rt->right); } G. Gini 2013/4 df - ordine posticipato - postorder T a b c e d f g c d b f g e a void postorder(tree rt) { if (rt == NULL) return; // Empty postorder(rt->left); postorder(rt->right); visit(rt); } G. Gini 2013/4 df - ordine simmetrico - inorder T a b c void inorder(tree rt) { if (rt == NULL) return; // Empty inorder(rt->left); visit(rt); inorder(rt->right); } G. Gini 2013/4 e d f g c b d a f e g in ampiezza T a b c e d f g Sequenza: a b e c d f g G. Gini 2013/4 Albero binario – programma in C typedef struct Tree { int data; struct Tree *left; struct Tree *right; } TREE; void insertNum(int i); void orderedInsert(TREE *, int); void deepFirstVisit(TREE *t); void breadthFirstVisit(TREE *t); int search(TREE *t, int i) ; TREE *root; G. Gini 2013/4 void main() { int i; do { printf("Inserire un intero (-1 = fine):"); scanf("%d",&i); if(i==-1) break; insertNum(i); } while(i!=-1); deepFirstVisit(root); printf("\n"); breadthFirstVisit(root); printf("\nInserire il valore da cercare: "); scanf("%d",&i); if(search(root,i)) printf("Trovato\n"); else printf("Niente da fare!\n"); } G. Gini 2013/4 void insertNum(int i) { if(root==NULL) { root=(TREE *) malloc(sizeof(TREE)); root->data=i; root->left=NULL; root->right=NULL; } else orderedInsert(root,i); } int search(TREE *t, int numero) { if(t==NULL) return 0; if(numero==t->data) return 1; if(numero > t->data) return search(t->right,numero); else return search(t->left,numero); } G. Gini 2013/4 void orderedInsert(TREE *nodo, int numero) { TREE *nuovo; if(numero == nodo->data) return; if(numero > nodo->data) { if(nodo->right == NULL) { nuovo=(TREE *) malloc(sizeof(TREE)); nuovo->data=numero; nuovo->left=NULL; nuovo->right=NULL; nodo->right=nuovo; } else orderedInsert(nodo->right, numero); } else { if(nodo->left == NULL) { nuovo=(TREE *) malloc(sizeof(TREE)); nuovo->data=numero; nuovo->left=NULL; nuovo->right=NULL; nodo->left=nuovo; } else orderedInsert(nodo->left, numero); }} G. Gini 2013/4 void deepFirstVisit(TREE *t) { if(t==NULL) return; deepFirstVisit(t->left); deepFirstVisit(t->right); printf("%d - ",t->data); } void breadthFirstVisit(TREE *t) { if(t==NULL) return; printf("%d - ",t->data); breadthFirstVisit(t->left); breadthFirstVisit(t->right); } G. Gini 2013/4 = postorder = preorder Alberi adatti per applicazioni di data base, di dizionari, etc G. Gini 2013/4 Alberi binari per rappresentare un insieme ordinato I valori nel sottoalbero di sinistra sono minori di quello del padre I valori nel sottoalbero di destra sono maggiori di quello del padre 47 25 11 7 17 77 43 31 44 G. Gini 2013/4 65 68 93 Esempi rappresentare con alberi binari diversi l’insieme ordinato dei numeri dispari da 1 a 11. 7 / \ 3 9 / \ \ 1 5 11 3 / \ 1 7 / \ 5 9 \ 5 / \ 3 9 / / \ 1 7 11 11 G. Gini 2013/4 Ricerca in albero binario ordinato Ricerca Ricerca del del valore valore 44 La La radice radice contiene contiene 66 44 << 66 44 sta sta nel nel sottoalbero sottoalbero sinistro sinistro di di 66 T 66 22 11 88 44 33 G. Gini 2013/4 Ricerca Ricerca del del valore valore 44 44 >> 22 44 sta sta nel nel sottoalbero sottoalbero destro destro di di 22 T 66 22 11 88 44 33 G. Gini 2013/4 Ricerca Ricerca del del valore valore 44 44 == 44 trovato! trovato! T 66 22 11 88 44 33 G. Gini 2013/4 Pseudocodice per ricerca --------------------------------------------------------boolean element-of?(item x , tree-item tree) --------------------------------------------------------if vuoto?(tree) then return false if x = radice(tree) then return true if x < radice(tree) then return element-of(x, sinistro(tree)) if x > radice(tree) then return element-of(x, destro(tree)) G. Gini 2013/4 Ricerca in C – come predicato typedef struct Nodo { Tipo data; struct Nodo *left; struct Nodo *right; } nodo; typedef nodo * tree; int searchItem ( Tipo item , tree node) { if ( node == NULL) return 0; If (item == node -> data ) return 1; if ( item < node -> data ) return searchItem ( item , node -> left ); else return searchItem ( item , node -> right ); } G. Gini 2013/4 Vantaggi alberi binari ordinati Per vedere se un elemento x è contenuto basta confrontare x con radice: se x è minore basta cercare nel sottoalbero sinistro, se maggiore nel destro. Quanto è vantaggioso? Se l'albero è bilanciato i sottoalberi conterranno la metà degli elementi, quindi ad ogni passo dimezziamo il numero dei nodi da esaminare. Il numero dei passi crescerà quindi come log2 n. G. Gini 2013/4 Dizionari Dizionario ✦Insieme dinamico che implementa le seguenti funzionalità ✦ • • • Cerca un item Inserisci un item Cancella un item Come implementarlo? ✦Array (ordinato) ✦Lista (non ordinata) ✦Albero binario di ricerca ✦ G. Gini 2013/4 Alberi binari di ricerca (ABR) ✦ ✦ Definizione ✦ Ogni nodo u contiene una chiave u.key associata ad un valore u.value ✦ Le chiavi appartengono ad un insieme totalmente ordinato Proprietà 1. Le chiavi dei nodi del sottoalbero sinistro di u sono minori di u.key 2. Le chiavi dei nodi del sottoalbero destro di u sono maggiori di u.key 3. Tutti nodi i del sottoalbero sinistro di u sono tali che per tutti nodi r del sottoalbero destro di u, l.key <= r.key G. Gini 2013/4 Alberi binari di ricerca (ABR) Proprietà di ricerca ✦Le proprietà 1. e 2. permettono di realizzare un algoritmo di ricerca dicotomica ✦ Proprietà di ordine ✦Come visitare l'albero per ottenere la lista ordinata dei valori? Depth first inorder ✦ implementazione ✦Ogni nodo deve mantenere ✦ Figlio sinistro, destro ✦Padre ✦Chiave ✦Valore ✦ G. Gini 2013/4 Padre Key, Value Figlio Figlio sinistro destro Ricerca - chiave ---------------------------------------------------------tree ricerca (item x , tree-item tree) --------------------------------------------------------if vuoto?(tree) then return NULL if x= tree.key then return tree if x < tree.key then return ricerca (x, left(tree)) if x > tree.key then return ricerca (x, right (tree)) G. Gini 2013/4 Ricerca del minimo e del massimo Sottoalbero con radice 2: Sottoalbero con radice 8: T - minimo 1 - massimo 4 - minimo 8 - massimo 15 66 22 88 44 11 12 12 4 99 33 G. Gini 2013/4 15 15 Ricerca minimo e massimo L’elemento minimo è quello più a sinistra ---------------------------------------item MINIMUM (tree-item x) --------------------------------------if x.left = NULL then return x return MINIMUM (x.left) L’elemento massimo quello più a destra ----------------------------------------item MAXIMUM (tree-item x) ----------------------------------------if x.right = NULL then return x return MAXIMUM (x.right) G. Gini 2013/4 … in forma iterativa Montresor usa notazione con . G. Gini 2013/4 Ricerca successore/predecessore Definizione ✦Il successore di un nodo u è il più piccolo nodo maggiore di u ✦ T 6 u PRIMO CASO ✦u ha un figlio destro ✦Il successore u' è il minimo del sottoalbero destro di u ✦ 2 1 12 4 Esempio: successore di 2 è 3 ✦ 3 u' G. Gini 2013/4 8 9 15 Ricerca successore/predecessore T u' SECONDO CASO ✦u non ha un figlio destro ✦Il successore è il primo avo u' tale per cui u sta nel sottoalbero sinistro di u' ✦ 66 22 11 Esempio: successore di 4 è 6 ✦ 88 12 12 33 44 u G. Gini 2013/4 99 15 15 Ricerca successore/predecessore G. Gini 2013/4 Ricerca: costo computazionale Le operazioni di ricerca sono confinate ai nodi posizionati lungo un singolo percorso dalla radice ad una foglia ✦Tempo di ricerca: dipende dall’altezza ✦caso pessimo? ✦ • L'albero è una sequenza lineare – n passi •caso ottimo? • albero è completo, o bilanciato. L'altezza dell'albero con n nodi è log n G. Gini 2013/4 h = altezza dell’albero Inserimento e rimozione • Quando si inserisce o si rimuove un elemento la struttura dell’albero cambia. • L’albero modificato deve mantenere le proprietà di un albero binario di ricerca. • L’inserimento risulta essere un’operazione immediata. • La rimozione di un elemento è più complicata, proprio perché bisogna essere certi che l’albero rimanga un albero binario di ricerca. • Dopo un certo numero di operazioni un albero si può sbilanciare G. Gini 2013/4 Inserimento 5 < 6 Inserimento chiave 5 Dovrà essere nel sottoalbero sinistro Si inserisce sempre come foglia 5 > 2 T 5 > 4 66 22 88 4 4 11 12 12 4 33 G. Gini 2013/4 55 99 15 15 Inserimento Inserire z con key = 14 9 5 15 4 2 1 7 6 16 12 8 11 3 13 y z 14 G. Gini 2013/4 19 18 21 Inserimento - esempio Inserire la sequenza <5, 4, 7, 2, 6, 8> 5 5 5 4 5 4 2 4 5 7 5 4 2 7 7 6 G. Gini 2013/4 4 2 7 6 8 Inserimento - esempio Inserire la sequenza <5, 4, 7, 2, 6, 8> 5 4 2 7 6 G. Gini 2013/4 8 Inserimento - esempio Inserire la sequenza <8, 7, 6, 4, 5, 2> 8 7 6 L’inserimento può portare ad avere alberi sbilanciati 4 2 La struttura dell’albero risulta diversa a seconda della sequenza di inserimento! 5 G. Gini 2013/4 Inserimento in C void orderedInsert(TREE *nodo, int numero) { TREE *nuovo; if(numero == nodo->data) return; if(numero > nodo->data) { if(nodo->right == NULL) { nuovo=(TREE *) malloc(sizeof(TREE)); nuovo->data=numero; nuovo->left=NULL; nuovo->right=NULL; nodo->right=nuovo; } else orderedInsert(nodo->right, numero); } else { if(nodo->left == NULL) { nuovo=(TREE *) malloc(sizeof(TREE)); nuovo->data=numero; nuovo->left=NULL; nuovo->right=NULL; nodo->left=nuovo; } else orderedInsert(nodo->left, numero); }} G. Gini 2013/4 inserimento in C tree orderedInsert(tree nodo, int numero) { tree *nuovo;/* ritorna il puntatore all’albero*/ if(numero == nodo->data) return nodo; if(numero > nodo->data) { /* va inserito a destra if(nodo->right == NULL) {/* non c’è figlio destro*/ nuovo=malloc(sizeof(tree)); nuovo->data =numero; nuovo->left =NULL; nuovo->right =NULL; nodo->right =nuovo; return nodo; } else return orderedInsert(nodo->right, numero); } else { /* va inserito a sinistra*/ if(nodo->left == NULL) { nuovo=malloc(sizeof(tree)); nuovo->data=numero; nuovo->left=NULL; nuovo->right=NULL; nodo->left=nuovo; return nodo; } else return orderedInsert(nodo->left, numero); }} G. Gini 2013/4 Inserimento (Cormen) con notazione funzionale scendere nell'albero fino a che non si raggiunge il posto in cui il nuovo elemento deve essere inserito, ed aggiungerlo come foglia TREE-INSERT(T, z) 1 y := NIL //padre 2 x := root(T) //figlio 3 while x ≠ NIL //cerca dove inserire 4 do y := x //memorizza il padre 5 if key (z)< key(x) 6 then x := left (x) //scende a sinistra 7 else x := right(x) //scende a destra secondo il confronto 8 z.p := y // inserisci z come figlio di y 9 if y = NIL 10 then root(T) := z //l'albero T e' vuoto e z diventa la radice 11 else if key(z) < key(y) 12 then left(y) := z 13 else right(y) := z G. Gini 2013/4 Complessità inserimento Per inserire z si usano due puntatori y e x. Il puntatore a x scende l’albero , y punta al padre di x. Nel ciclo while i due puntatori (x e y) scendono l’albero. x scende al figlio sinistro o destro a seconda dell’esito del confronto di key[z] con key[x]. Ci si ferma quando x = NIL e x occupa la posizione in cui z verrà inserito. I passi necessari per l’inserimento sono non più del cammino massimo tra la radice e una foglia (cioè l’altezza). Nel caso ricorsivo, il numero delle chiamate ricorsive dipende da h. G. Gini 2013/4 cancellazione Cancellare z, che ha chiave 14 Caso 1: z senza figli (foglia) – si rimuove 9 5 15 4 2 1 7 6 16 12 8 11 3 z 14 G. Gini 2013/4 19 13 18 21 cancellazione Caso 2: cancellare z (che ha chiave 16) con 1 figlio – si riattacca il figlio al padre di z 9 15 5 4 2 1 7 6 z 16 12 8 11 3 19 13 14 G. Gini 2013/4 18 21 cancellazione Caso 3: cancellare z (che ha chiave 12) con 2 figli. 9 15 5 4 2 1 7 6 3 16 z 12 8 11 y 13 Successore di z: Viene rimosso e va al posto di z. G. Gini 2013/4 19 14 18 21 cancellazione Caso 3: z con 2 figli. 9 5 15 4 2 1 7 6 16 y 13 8 11 19 14 3 18 z 12 G. Gini 2013/4 21 cancellazione tre casi: 1. Se z non ha figli, allora si modifica puntatore a z, che punta non più a z ma a NIL. 2. Se z ha un unico figlio, allora si taglia fuori z dall’albero, facendo puntare il puntatore a z all’unico figlio di z. 3. Se z ha due figli, allora si individua il successore, ossia il minimo del suo sottoalbero destro. 1. Il successore y ha nessun figlio o 1 figlio. Quindi y prende il posto di z, riconducendosi al caso 1 e 2. Alla fine i dati in y vengono copiati in z. G. Gini 2013/4 Passi necessari • L’operazione di rimozione può richiede che SUCCESSOR(z) venga eseguito. • SUCCESSOR(z) richiede tempo che dipende da h anche TREE-INSERT() e TREE-DELETE() richiedono tempo che dipende dall’altezza G. Gini 2013/4 Cancellazione pseudocodice TREE-DELETE(T, z) 1 if left (z)= NIL or right(z) = NIL 2 then y := z // z ha 0 o 1 figlio, copialo in y 3 else y := TREE-SUCCESSOR(z) // z ha 2 figli, trova succ(z) 4 if left (y)≠ NIL // y nodo da eliminare 5 then x := left(y) // x figlio di y 6 else x := right (y) G. Gini 2013/4 Cancellazione -continua 7 if x ≠ NIL then // se y ha il figlio 8 then p(x) := p(y) // taglia fuori y e aggiorna il padre 9 if p (y)= NIL 10 then root (T):= x // se y è la radice, x diventa radice 11 else if y = left(p(y)) // collega x al padre di y 12 then left(p(y)) := x 13 else right(p(y)) := x 14 if y ≠ z then // se y è il successore 15 key (z):= key(y) // ricopia i dati di y in z 16 return y G. Gini 2013/4 Cancellazione –correttezza Caso 1 - nessun figlio ✦Eliminare foglie non cambia la proprietà di ordine dei nodi rimanenti ✦ Caso 2 - solo un figlio (destro o sinistro) ✦Se u è il figlio destro (sinistro) di p, tutti i i valori nel sottoalbero destro sono maggiori (minori) di p ✦Quindi f può essere attaccato come figlio destro (sinistro) di p al posto di u ✦ Caso 3 - due figli ✦Il successore s ✦ è sicuramente maggiore di tutti i nodi del sottoalbero sinistro di u ✦è sicuramente minore di tutti i nodi del sottoalbero destro di u ✦ Quindi può essere sostituito a u ✦ G. Gini 2013/4 Modifica: costo computazionale Le operazioni di modifica sono confinate ai nodi posizionati lungo un singolo percorso dalla radice ad una foglia ✦In tempo di ricerca dipende dall’altezza dell’albero •Caso pessimo: l'albero è organizzato come una sequenza lineare - n •Caso ottimo: l'albero è completo, o bilanciato. In questo caso, l'altezza dell'albero è log n ✦ G. Gini 2013/4 h = altezza dell’albero bilanciamento Fattore di bilanciamento ✦Il fattore di bilanciamento β(v) di un nodo v è la massima differenza di altezza fra i sottoalberi di v ✦ Esempio: albero perfetto: ✦β(v)=0 per ogni nodo v ✦ In generale: ✦Sono necessarie tecniche per mantenere bilanciato l'albero ✦ Vedremo alberi rosso-neri ✦ G. Gini 2013/4 Per approfondire • Stanford Encyclopedia of philosophy • Temi generali (Recursion, computer science, ..) • Autori G. Gini 2013/4 Esercizio 1 • 1. 2. Sia data la seguente struttura per la memorizzazione di alberi binari etichettati con numeri interi: typedef struct nodo { int info; struct nodo *left, *right; } NODO; typedef NODO *tree; scrivere due funzioni ricorsive int sommaNodi(tree t), che somma i valori delle etichette nell'albero int cercaMax(tree t), che cerca il valore dell'etichetta massima dell'albero. G. Gini 2013/4 Soluzione 1 int sommaNodi(tree t) { if (t==NULL) return 0; return t->info+sommaNodi(t->left)+sommaNodi(t->right);} int max(int a,int b) { if(a>b) return a; else return b;} int max3(int a,int b,int c) { return max(a,max(b,c));} int cercaMax(tree t) { if (t==NULL) return 0; if (t->left==NULL && t->right==NULL) return t->info; if(t->left==NULL) return max(t->info, cercaMax(t->right)); if(t->right==NULL) return max(t->info, cercaMax(t->left)); return max3(cercaMax(t->right), cercaMax(t->left), t->info); } G. Gini 2013/4 Esercizio 2 Un albero binario si dice isobato se tutti i cammini dalla radice alle foglie hanno la stessa lunghezza • Data la seguente definizione di albero typedef struct EL { int dato; struct EL *left; struct EL *right; } Node; typedef Node *tree; codificare una funzione che riceve in input un albero e restituisce 1 se l’albero è isobato, 0 altrimenti. G. Gini 2013/4 Soluzione 2 Uso funzione ausiliaria int contaProfonditaSeUguale(tree t) { int s, d; if(t==null) return 0; s=contaProfonditaSeUguale(t->left); d=contaProfonditaSeUguale(t->right); if(d==-1||s==-1) return -1 if(d==s) return d+1; if(d==0) return s+1; if(s==0) return d+1; } return -1;//d!=s int isobato(tree t) { if(contaProfonditaSeUguale(t)==-1) return 0; else return 1; } G. Gini 2013/4 Esercizio 3 • • • Si consideri la seguente definizione di un albero binario typedef struct EL { int dato; struct EL * left, right; } node; typedef node * tree; Definiamo un albero come "artussiano" se è composto solo da nodi foglia nodi con un solo figlio nodi con due figli aventi lo stesso numero di discendenti Codificare una funzione che riceve in input un albero e restituisce 1 se l’albero è "artussiano", 0 altrimenti. Nel risolvere il problema è consigliato servirsi di opportune funzioni ausiliarie. G. Gini 2013/4 Soluzione 3 int contaNodi ( tree t ) { if ( t == NULL ) return 0; else return (contaNodi(t->left) + contaNodi(t->right) + 1); } int artussiano(tree t) { if(t==NULL) return 1; if(t->left==NULL && t->right==NULL) return 1; if(t->left==NULL) return artussiano(t->right) if(t->right==NULL) return artussiano(t->left) if(contaNodi(t->left)==contaNodi(t->right) && artussiano(t->left) && artussiano(t->right)) return 1; return 0; } G. Gini 2013/4 Esercizio 4 • Si consideri un albero binario in cui il valore dei nodi è sempre positivo. • Supponiamo che percorrendo un cammino dalla radice alle foglie si totalizzi un punteggio pari alla somma dei valori contenuti nei nodi percorsi. • - Scrivere una funzione maxPunti che calcola il punteggio massimo che possiamo totalizzare percorrendo un cammino dalla radice alle foglie. • -Vogliamo percorrere l’albero dalla radice ad una foglia totalizzando esattamente un certo punteggio. Scrivere una funzione esisteCammino che dati un albero ed un intero k, dice se esiste un cammino dalla radice ad una foglia che permette di totalizzare esattamente k punti. G. Gini 2013/4 Soluzione 4 int maxPunti ( tree t ) { int D, S; if (t == NULL) return 0; S = maxPunti( t->left ); D = maxPunti( t->right ); if ( S > D ) return S + t->dato; else return D + t->dato; } int esisteCammino ( tree t, int k ) { int D, S; if (t == NULL && k==0) return 1; if (t == NULL) return 0; if (k – t->dato <0) return 0; return (esisteCammino(t->left, k - t->dato) || esisteCammino(t->right, k - t->dato)); } G. Gini 2013/4 Esercizio 5 • • • Si consideri la seguente definizione di un albero binario typedef struct EL { int dato; struct EL * left, right; } node; typedef node * tree; Scrivere una funzione che riceve il puntatore alla radice di un albero e lo scandisce interamente costruendo una lista tale che abbia tanti nodi quante sono le foglie dell’albero e che ogni nodo della lista punti ad una diversa foglia dell’albero. La funzione deve restituire al chiamante il puntatore all’inizio della lista creata. typedef struct ELLista { tree foglia; struct ELLista * next; } nodeLista; typedef nodeLista * Lista; Si noti che esistono diversi modi di visitare l’albero che originano diverse liste come risultato. G. Gini 2013/4 Soluzione 5 void creaLista(tree t, Lista *lis) { Lista temp; if(t==NULL) return; if(t->left==NULL && t->right==NULL) { temp=*lis; *lis=(Lista)malloc(sizeof(NodoLista)); *lis->foglia=t; *lis->next=temp; } if(t->left!=NULL) creaLista(t->left, lis); if(t->right!=NULL) creaLista(t->right, lis); } G. Gini 2013/4 Esercizio 6 • Si definisca una struttura dati adatta a rappresentare un albero Nario. Per semplicità si consideri il caso in cui i nodi contengono, come dati utili, dei semplici numeri interi. • Ogni nodo contiene, invece di una coppia di puntatori a nodi, come nel caso degli alberi binari, una lista di puntatori a nodo. Tale lista è una lista concatenata semplice, realizzata tramite la struttura Ramo. typedef Nodo * Albero; typedef struct Branch { Albero child; struct Branch * next; } Ramo; typedef struct Knot { int dato; struct Branch * rami; } Nodo; • Si progetti la funzione int conta( … ) che conta il numero di nodi di un albero N-ario. G. Gini 2013/4 soluzione 6 int contaNodi ( Albero t ) { if ( t == NULL ) return 0; else return 1 + contaRami( t->rami ); } int contaRami ( Ramo * b ) { if ( b == NULL ) return 0; else return contaNodi( b->child ) + contaRami( b->next ); } G. Gini 2013/4