ADT ALBERO BINARIO (tree)! n Rappresentazione collegata (puntatori a strutture) 1 ADT ALBERO BINARIO OPERAZIONI PRIMITIVE DA REALIZZARE Operazione! Descrizione! cons_tree: D x tree x tree Costruisce un nuovo albero, che ha -> tree! l’elemento fornito come radice con figli i due sottoalberi dati! root: tree -> D! Restituisce l’elemento radice dell’albero! left: tree -> tree! Restituisce il sottoalbero sinistro ! right: tree -> tree! Restituisce il sottoalbero destro! emptytree: -> tree! Restituisce (costruisce) l’albero vuoto! empty: tree-> boolean! ! Restituisce vero se l’albero dato è vuoto, falso altrimenti! 2 ADT ALBERO BINARIO OPERAZIONI DERIVATE (etc etc) Operazione! Descrizione! preorder: tree! Visita in preordine (ordine anticipato)! inorder: tree! Visita in ordine (“” simmetrico)! postorder: tree! Visita in postordine (“” ritardato)! ! member: D x tree ->! boolean ! Ricerca di un elemento nell’albero! height: tree -> int! Calcola l’altezza di un albero! nodi: tree -> int ! Conta il numero dei nodi! 3 Esercizio 10.3.0! n Un file binario ad accesso diretto (PERSONE.DAT) contiene un elenco di informazioni su persone (cognome, nome, data di nascita, città di residenza). Il cognome di ciascuna persona costituisce la chiave unica di tale elenco. Per effettuare ricerche su questo elenco, si costruisca un albero binario di ricerca mantenendo, per ciascun cognome il numero di record corrispondente. Si definisca un programma C che: a) costruisca la struttura dati in memoria principale a partire dal file PERSONE.DAT e stampi l’elenco ordinato dei cognomi; b) calcoli l'altezza dell’albero così ottenuto e il numero dei suoi nodi; c) letto un cognome a terminale, verifichi se esiste un elemento con quella chiave nell’albero e, trovatolo, acceda al file e legga il record corrispondente, visualizzando i campi nome, datan e città; d) letto un cognome a terminale, stampi ordinatamente a video i cognomi minori o uguali di quello letto. 4 $ Esercizio 10.3.0! Si modifichino i file main.c e tree.c già disponibili, implementando le operazioni richieste in funzione di quelle esportate dall’ADT degli elementi Il main da realizzare deve: a) leggere il contenuto del file ricordando il numero del blocco letto e inserendo chiave, posizione come elemento in un albero binario di ricerca, e stampare l’elenco ordinato dei cognomi; b) calcolarne altezza e numero di nodi; c) letto un cognome e accedendo all’albero recuperare le informazioni complete di quel cognome dal file; d) stampare i cognomi minori di quello letto. COMPONENTI! el.h tree.h tree.c main.c el.c ADT ELEMENT: el.h! /* ELEMENT TYPE - file el.h*/ typedef struct { char nome[15]; char cognome[15]; char datan[7]; char citta[15]; } record_type; typedef struct { char cognome[15]; int pos;} el_type; typedef enum {false,true} boolean; /* operatori esportabili */ boolean isequal(el_type, el_type); boolean isless(el_type, el_type); void showel (el_type); ADT ELEMENT: el.c! /* ELEMENT TYPE - file el.c*/ #include <stdio.h> #include <string.h> #include "el.h" boolean isequal(el_type e1, el_type e2) /* due elementi uguali se stesso cognome */ { if (strcmp(e1.cognome,e2.cognome)==0) return true; else return false; } boolean isless(el_type e1, el_type e2) { if (strcmp(e1.cognome,e2.cognome) <0) return true; else return false; } void showel (el_type e) { printf("%s\t%d\n",e.cognome,e.pos); } ADT TREE: tree.h! /* TREE INTERFACE - file trees.h*/ #include "el.h“ typedef struct nodo { el_type value; struct nodo *left, *right; } NODO; typedef NODO * tree; /* operatori esportabili */ boolean empty(tree t); tree emptytree(void); el_type root(tree t); tree left(tree t); tree right(tree t); tree cons_tree(el_type e, tree l, tree r); tree ord_ins(el_type e, tree t); void inorder(tree t); int height (tree t); int nodi (tree t); int member_ord(el_type e, tree t); /*NOTA TIPO RESTITUITO */ void stampa_minori(tree t, el_type e); int max_int(int, int); $ TO DO! Si modifichino i file main.c e tree.c già disponibili, implementando le operazioni richieste in funzione di quelle esportate dall’ADT degli elementi Mumble mumble … Il main da realizzare deve: a) leggere il contenuto del file ricordando il numero del blocco letto e inserendo chiave, posizione come elemento in un albero binario di ricerca, e stampare l’elenco ordinato dei cognomi; b) calcolarne numero di nodi e altezza; c) letto un cognome e accedendo all’albero recuperare le informazioni complete di quel cognome dal file; d) stampare i cognomi minori di quello letto. main.c (1 - stub) #include <stdio.h> #include <string.h> #include <stdlib.h> #include "tree.h" main() { tree indice=emptytree(); int pos; FILE *f; record_type REC; el_type EL; char COGNOME[15]; f = fopen("PERSONE.DAT", "rb"); if(f==NULL){ printf("errore apertura file\n"); exit(-1); } pos=0; /* CICLO DI SCANSIONE DEL FILE */ while(fread(&REC,sizeof(record_type),1,f)>0){ // “definizione” campi dell’elemento EL // inserimento EL nell’albero indice // da implementare } main.c (2 - stub) fclose(f); printf("Chiusura del file\n"); printf("Stampa dell’albero\n"); inorder(indice); /* CALCOLO ALTEZZA E NUM.NODI DELL’ALBERO */ printf("Altezza dell'albero: %d\n", height(indice)); printf("Num. Nodi dell'albero: %d\n", nodi(indice)); $ TO DO – passo 1! Si modifichino i file main.c e tree.c già disponibili, implementando le operazioni richieste in funzione di quelle esportate dall’ADT degli elementi Mumble mumble … Il main da realizzare deve: a) leggere il contenuto del file ricordando il numero del blocco letto e inserendo chiave, posizione come elemento in un albero binario di ricerca, e stampare l’elenco ordinato dei cognomi; b) calcolarne numero di nodi e altezza; c) letto un cognome e accedendo all’albero recuperare le informazioni complete di quel cognome dal file; d) stampare i cognomi minori di quello letto. tree.c (1) #include <stdlib.h> #include "tree.h“ boolean empty(tree t) /* test di albero vuoto */ { return (t==NULL); } tree emptytree(void) /* inizializza un albero vuoto */ { return NULL; } 14 tree.c (2) el_type root (tree t) /* restituisce la radice dell'albero t */ { if (empty(t)) abort(); else return(t->value); } tree left (tree t) /* restituisce il sottoalbero sinistro */ { if (empty(t)) return(NULL); else return(t->left); } tree right (tree t) /* restituisce il sottoalbero destro */ { if (empty(t)) return(NULL); else return(t->right); } 15 tree.c (3) tree cons_tree(el_type e, tree l, tree r) /* costruisce un albero che ha nella radice e; per sottoalberi sinistro e destro l ed r rispettivamente */ { tree t; t = (NODO *) malloc(sizeof(NODO)); t-> value = e; t-> left = l; t-> right = r; return (t); } 16 tree.c (4) void inorder(tree t) { if (! empty(t)) { inorder(left(t)); showel( root(t) ); inorder(right(t)); } } 17 tree.c (5 – ord_ins) tree ord_ins(el_type e, tree t) /* albero binario di ricerca senza duplicazioni */ { if (empty(t)) /* inserimento */ return cons_tree(e,emptytree(),emptytree()); else { if (isless(e,root(t))) t->left = ord_ins(e,left(t)); else t->right = ord_ins(e,right(t)); return t; } } 18 BST – Inserimento iterativo! n L’algoritmo di inserimento è simile a quello di ricerca • Cerca, nell’albero, la posizione corretta di inserimento, ovvero il nodo che diventerà il padre di quello da inserire • Una volta trovato il nodo, appende il nuovo nodo come figlio sinistro/destro in modo da mantenere la proprietà di ordinamento dell’albero Inserimento di 16 8 3 2 12 5 15 6 13 18 16 19 $ Inserimento iterativo tree ord_ins_it(el_type e,tree root) {tree p, t=root; if (root==NULL) return cons_tree(e,NULL,NULL); else { while (t!=NULL) if (isLess(e,t->value)) {p=t; t=t->left;} else {p=t; t=t->right;} } //p punta al padre if (isLess(e,p->value)) p->left = cons_tree(e,NULL,NULL); else p->right = cons_tree(e,NULL,NULL); return root; } 20 main.c (soluzione) #include <stdio.h> #include <string.h> #include <stdlib.h> #include "trees.h" main() { tree indice=emptytree(); int pos; FILE *f; record_type REC; el_type EL; char COGNOME[15]; f = fopen("PERSONE.DAT", "rb"); if(f==NULL){ printf("errore apertura file\n"); exit(-1); } pos=0; /* CICLO DI SCANSIONE DEL FILE */ while(fread(&REC,sizeof(record_type),1,f)>0) { strcpy(EL.cognome,REC.cognome); EL.pos=pos; /* INSERIMENTO NELL'ALBERO */ printf("Inserimento nell’albero di %s con posizione %d\n", EL.cognome, pos); indice = ord_ins(EL,indice); pos++; } main.c (2 - soluzione) fclose(f); printf("Chiusura del file\n"); printf("Stampa dell’albero\n"); inorder(indice); /* CALCOLO ALTEZZA E NUM.NODI DELL’ALBERO */ printf("Altezza dell'albero: %d\n", height(indice)); printf("Num. Nodi dell'albero: %d\n", nodi(indice)); $ TO DO – passo 2! Si modifichino i file main.c e tree.c già disponibili, implementando le operazioni richieste in funzione di quelle esportate dall’ADT degli elementi Il main da realizzare deve: a) leggere il contenuto del file ricordando il numero del blocco letto e inserendo chiave, posizione come elemento in un albero binario di ricerca, e stampare l’elenco ordinato dei cognomi; b) calcolarne numero di nodi e altezza; c) letto un cognome e accedendo all’albero recuperare le informazioni complete di quel cognome dal file; d) stampare i cognomi minori di quello letto. CONTARE I NODI (Albero binario) Algoritmo (per un albero binario) n se l'albero è vuoto, i nodi sono 0 n altrimenti, i nodi sono 1 (la radice) + quelli del figlio sinistro + quelli del figlio destro 1 nodo (radice) Numero di nodi: nL Numero di nodi: nR sottoalbero sinistro Totale nodi: 1 + nL + nR sottoalbero destro tree.c (7) Int nodi(tree t) { if (empty(t)) return 0; else return (1+nodi(left(t))+nodi(right(t))); } n E’ tail ricorsiva? 25 ALTEZZA DI UN ALBERO ALTEZZA DI UN ALBERO (Albero binario) Algoritmo (per un albero binario) n se l'albero è vuoto, l’altezza è nulla (0) n altrimenti, è il massimo tra l’altezza_aux del figlio sinistro e l’altezza_aux del figlio destro 1 arco (lo conto in altezza_aux, solo se figlio non vuoto) Altezza_aux L Altezza_aux R sottoalbero sinistro sottoalbero destro Altezza: 1+ max(height_aux(L),height_aux(R)) $ To Do: altezza di un albero Altezza di un albero, lunghezza del cammino più lungo dalla radice a una delle foglie Algoritmo: n se l'albero è vuoto, o ha solo un nodo (radice) la sua altezza è 0 n altrimenti, è 1 + max( altezza del figlio sinistro , altezza del figlio destro ) n 3 8 11 2 tree.c (8 - height) int height (tree t) { if (empty(t)) return 0; else return max_int( height_aux(left(t)), height_aux(right(t))) ; } int height_aux(tree t) { if (empty(t)) return 0; else return (1 + max_int( height_aux(left(t)), height_aux(right(t)) )); } n E’ tail ricorsiva? 29 $ To Do: altezza di un albero 9 8 14 7 6 5 3 4 13 11 $ TO DO – passo 3! Si modifichino i file main.c e tree.c già disponibili, implementando le operazioni richieste in funzione di quelle esportate dall’ADT degli elementi Il main da realizzare deve: a) leggere il contenuto del file ricordando il numero del blocco letto e inserendo chiave, posizione come elemento in un albero binario di ricerca, e stampare l’elenco ordinato dei cognomi; b) calcolarne numero di nodi e altezza; c) letto un cognome e accedendo all’albero recuperare le informazioni complete di quel cognome dal file; d) stampare i cognomi minori di quello letto. main.c (3 - stub) /* VISUALIZZAZIONE RECORD */ printf("Inserisci un cognome: "); scanf("%s", COGNOME); f = fopen("PERSONE.DAT", "rb"); if(f==NULL){ printf("errore apertura file\n"); exit(-1); } // invocare opportunamente member_ord, // trovare il record sul file con fseek // e stampare risultato // invocare opportunamente stampa_minori } /* VISUALIZZAZIONE RECORD */ printf("Inserisci un cognome: "); scanf("%s", COGNOME); f = fopen("PERSONE.DAT", "rb"); if(f==NULL){ printf("errore apertura file\n"); exit(-1); } strcpy(EL.cognome, COGNOME); pos = member_ord(EL, indice); if (pos>=0){ fseek(f, pos*sizeof(record_type), SEEK_SET); if(fread(&REC,sizeof(record_type),1,f)>0){ printf("Posizione %d: Cognome:%s\tNome:%s\tDatan: %s\tCitta:%s\n", pos,REC.cognome, REC.nome, REC.datan, REC.citta); } stampa_minori(indice, EL); } } Ricerca binaria - iterativa! 1. 2. Sia t un puntatore ad albero binario (tree) e gli sia inizialmente assegnata la radice dell’albero Finché t non è nullo e la sua radice non contiene il valore cercato, confrontare il valore contenuto nella radice di t con il valore cercato a. Se è uguale, restituire il valore trovato b. Se è minore, assegnare a t il figlio destro e procedere con 2 c. Se è maggiore, assegnare a t il figlio sinistro e procedere con 2 34 Ricerca (iterativa) in BST! n E’possibile decidere, per ogni nodo, se proseguire la ricerca a sinistra o a destra (ricerca binaria), aggiornando il puntatore t Ricerca di 13 8 3 2 12 5 15 6 13 18 35 $ TO DO – Passo 3! Si modifichino i file main.c e tree.c già disponibili, implementando le operazioni richieste in funzione di quelle esportate dall’ADT degli elementi Il main da realizzare deve: a) leggere il contenuto del file ricordando il numero del blocco letto e inserendo chiave, posizione come elemento in un albero binario di ricerca, e stampare l’elenco ordinato dei cognomi; b) calcolarne numero di nodi e altezza; c) letto un cognome e accedendo all’albero recuperare le informazioni complete di quel cognome dal file; d) stampare i cognomi minori di quello letto. /* VISUALIZZAZIONE RECORD */ printf("Inserisci un cognome: "); scanf("%s", COGNOME); f = fopen("PERSONE.DAT", "rb"); if(f==NULL){ printf("errore apertura file\n"); exit(-1); } strcpy(EL.cognome, COGNOME); pos = member_ord(EL, indice); if (pos>=0){ fseek(f, pos*sizeof(record_type), SEEK_SET); if(fread(&REC,sizeof(record_type),1,f)>0){ printf("Posizione %d: Cognome:%s\tNome:%s\tDatan: %s\tCitta:%s\n", pos,REC.cognome, REC.nome, REC.datan, REC.citta); } // stampa_minori(indice, EL); } } tree.c (7) int member_ord(el_type e,tree t) { if (empty(t)) return -1; else if (isequal(e,root(t)) return (t->value).pos; else if (isless(e,root(t)) return member_ord(e,left(t)); else return member_ord(e,right(t)) ; } n E’ tail ricorsiva? 39 tree.c (6 – member_ord) int member_ord(el_type e, tree t) { while (t!=NULL){ if (isless(e,root(t))) t=t->left; else if (isequal(e,root(t))) return (t->value).pos; else t=t->right; } return -1; } 40 $ Esercizio 10.3.0! Si modifichino i file main.c e tree.c già disponibili, implementando le operazioni richieste in funzione di quelle esportate dall’ADT degli elementi Il main da realizzare deve: a) leggere il contenuto del file ricordando il numero del blocco letto e inserendo chiave, posizione come elemento in un albero binario di ricerca, e stampare l’elenco ordinato dei cognomi; b) calcolarne numero di nodi e altezza; c) letto un cognome e accedendo all’albero recuperare le informazioni complete di quel cognome dal file; d) stampare i cognomi minori di quello letto. Stampa_minori Montale STAMPA MINORI Montale Hesse Alberoni Pennac Marquez sottoalbero sinistro Ziliotto sottoalbero destro Stampa_minori Montale STAMPA MINORI Montale Hesse Alberoni Marquez sottoalbero sinistro n n Pennac Ziliotto sottoalbero destro Modifica della visita inorder; stampo il contenuto del nodo solo se il cognome precede quello dato Non efficientissima (visito tutti i nodi così, anche quelli con cognome sicuramente maggiore) tree.c void stampa_minori(tree t, el_type e) /* ricorsiva – visita in ordine simmetrico */ { if (!empty(t)) { stampa_minori(left(t),e); if (isless(root(t),e)) showel(root(t)); stampa_minori(right(t),e); } } UN PO’ DI CONSIDERAZIONI …! n Quanto è bilanciato l’albero binario di ricerca ottenuto? n Quanti nodi? à 10 Quale altezza? à 6 n Ci sono molti algoritmi per bilanciare alberi non bilanciati non bilanciato bilanciato Alberi bilanciati! n Il problema di BST è che normalmente NON sono bilanciati: • Inserimenti e cancellazioni sbilanciano l’albero • Se l’albero non è correttamente bilanciato le operazioni (tutte) costano “parecchio” n Soluzione: alberi che si autobilanciano • AVL (Adel'son-Vel'skii-Landis) • Red-Black • … 52 Alberi AVL! n n n n Un albero AVL è un Albero Binario di Ricerca bilanciato Un nodo si dice bilanciato quando l’altezza del sotto-albero sinistro differisce dall’altezza del sotto-albero destro di al più una unità Un albero si dice bilanciato quando tutti i nodi sono bilanciati Le operazioni sono le stesse che si possono eseguire su un albero binario di ricerca 53 Rotazioni! n Si supponga di disporre di un albero sbilanciato – è possibile bilanciarlo tramite opportune rotazioni Ruotare il nodo 12 verso sinistra in 8 modo che diventi la radice dell’albero à il padre di 12 (8) diventa il suo sotto-albero sinistro 12 15 13 18 16 20 54 Rotazioni! n Rotazione 1: il nodo 12 diventa la radice Ruotare il nodo 15 verso sinistra in modo che diventi la radice dell’albero à il padre di 15 (12) diventa il suo sotto-albero sinistro, il sotto-albero sinistro di 15 (13) diventa il sotto-albero destro di 12 12 8 15 13 18 16 20 55 Rotazioni! n Rotazione 2: il nodo 15 diventa la radice e l’albero risulta bilanciato 15 12 8 18 13 16 20 56 Alberi AVL! n n n n Si supponga di partire con un albero bilanciato, secondo la definizione data in precedenza Una serie di inserimenti/cancellazioni può sbilanciare l’albero Opportune rotazioni sono in grado di ribilanciare l’albero Naturalmente i costi di inserimento/cancellazione crescono di molto ma la ricerca rimane sempre molto efficiente! (O(log2 N) se N nodi) 57 Per giocare un po’…! http://www.qmatica.com/DataStructures/Trees/AVL/AVLTree.html n Applet java per giocare con alberi di ricerca di vario tipo, con possibilità di inserire, cancellare, ruotare e vedere come si comportano i vari tipi di alberi supportati http://cprogramminglanguage.net/avl-tree.aspx n Realizzazione modulare in C delle operazioni su AVL tree (insert, delete da fare) n … ma anche RedBlack tree 58 59 Esercizi molto consigliati…! • Scrivere la ricerca in albero binario di ricerca in modo iterativo • Calcolare l’altezza di un albero • Calcolare il bilanciamento di un nodo (differenza fra le altezze dei sotto-alberi sinistro e destro) 60