LABORATORIO DI ALGORITMI E STRUTTURE DATI A-L Ingegneria e scienze informatiche – Cesena A.A: 2016/2017 Docente: Greta Sasso Alberi binari di ricerca Albero binario in cui le chiavi dei nodi sono ordinate secondo i seguenti principi: Le chiavi del sottoalbero sinistro di un nodo X sono tutte strettamente minori della chiave del nodo X Le chiavi nel sottoalbero destro di un nodo X sono tutte strettamente maggiori della chiave del nodo X Non esistono chiavi duplicate nell’albero Queste proprietà hanno lo scopo di agevolare la ricerca di un elemento. Alberi binari di ricerca typedef struct BINARYTREE { int key; struct BT * left; struct BT * right; }BT; BT * createNode( int e){ BT * node= (BT *)malloc(sizeof (BT)); if (node!=NULL){ node->key=e; node->left=NULL; node->right=NULL; } return node; } Creazione di un BST da una lista di interi Creiamo l’albero binario di ricerca, visitando sequenzialmente la lista di interi in input. Lista di partenza: Creazione a partire da un vettore BT* createTree( int * vet , int dim) { BT* T = NULL; int i = 0; if (dim > 0) { T = nodeAlloc(vet[0]); for (i = 1; i < dim; i++) { insertKey(T, vet[i]); } } return T; } void insertKey(BT* T, int key) { if (key < T-> elem) { // se minore della radice lo inserisco nel sottoalbero di sinistra if (T->left == NULL) T->left = nodeAlloc(key); else insertKey(T->left, key); }else if (key > T->elem) {//altrimenti se maggiore della radice lo inserisco nel sottoalbero di sinistra if (T->right == NULL) T->right = nodeAlloc(key); else insertKey(T->right, key); } } Visita di un albero binario di ricerca Si possono applicare le tre tipologie di visita in profondità viste per gli alberi binari La visita in-order restituisce la lista ordinata delle chiavi di un albero binario di ricerca: Visita del sottoalbero di sinistra Visita della radice Visita del sottoalbero di destra Ricerca di un elemento Ricerca in un albero binario: 8<15? Si per cercare un elemento scendo ricorsivamente nei sottoalberi di sinistra e di destra fino a giungere ai casi base: albero terminato, o elemento trovato. No 15==15 Trovato! if (tree == NULL)return 0; if (tree->elem == s) return 1; return treeSearch(tree->left,s) + treeSearch(tree->right,s); 18<15? Elem da cercare Ricerca in un albero binario di ricerca: Possiamo scegliere quale dei due sottoalberi analizzare per continuare la ricerca dell’elemento dato. 15 Ricerca di un elemento Si confronta l’elemento da cercare con il valore contenuto nella radice: Se è minore allora si scende nel sottoalbero di sinistra Se è uguale la ricerca ha successo Se è maggiore si scende nel sottoalbero di destra Il procedimento si ripete finché la rimanente parte da analizzare corrisponde a un albero vuoto o finché non si trova l’elemento( caso base della ricorsione) I nodi incontrati durante la ricorsione Percorrono un cammino verso le foglie. Pertanto Il tempo di esecuzione è O(height(T)) search(x,k) if x==NIL or k==x.key return x if key< x.key return search(x.left,k) else return search(x.right,k) Ricerca del massimo Che caratteristiche ha il massimo assoluto rispetto al massimo di un sottoalbero? 15 è un massimo locale 18 è il massimo assoluto il massimo assoluto non ha il figlio di destra perché non ci sono elementi maggiori Massimo Assoluto Massimo nel sottoalbero con chiave 18 max(T,e) While(x.right!=NIL) x=x.right Return x Quanto costano queste operazioni? Nel caso pessimo, visitano il percorso più lungo radice-foglia: O(Height[T]) Predecessore Predecessore di un dato nodo: cerca il nodo con chiave maggiore tra tutti i nodi con chiave strettamente minore di quella del nodo dato. Due i casi da analizzare 1) Il nodo ha un figlio sinistro il predecessore è il massimo nel sottoalbero sinistra. Il massimo non ha un figlio alla sua destra infatti. 2) Il nodo non ha un figlio sinistro il predecessore è l’antenato più vicino che contiene il nodo in questione nel suo sottoalbero destro ( non esiste se e solo se il nodo è il minimo assoluto) • Per semplificare l’implementazione delle funzioni predecessore e successore abbiamo la necessità di modificare la struttura dati. • È necessario introdurre un nuovo puntatore che permetta di accedere velocemente al padre di un nodo. • Occorre modificare la funzione di inserimento di un nodo nell’albero tenendo conto di questo nuovo fattore. • Le altre funzioni di base come la ricerca di una chiave , o la cancellazione dell’albero non necessitano di modifiche struct BINARYTREE { int key; struct NODO* left; struct NODO* right; struct NODO* up; }; void insertKey2(BT2* T, int key) { if (key < T->key) { if (T->left == NULL) { T->left = nodeAlloc(key); T->left->up = T; } else insertKey(T->left, key); } else if (key > T->key) { if (T->right == NULL){ T->right = nodeAlloc(key); T->right->up = T; } else insertKey(T->right, key); } } • Aggiorniamo quindi la procedura di inserimento per gestire il puntatore al padre Predecessore pseudocodice iterativo function SearchPredecessor(T) if T = NIL then return NIL end if if T.left != NIL then //CASO 1: ha un figlio sinistro return SearchMax(T.left) else //CASO 2: cerco l’antenato più vicino che contiene l’attuale nodo nel suo sottoalbero di destra X=T y = T.up //antenato while y != NULL and x == y.left do x = y y = x.up end while return y end if end function 2) Il nodo non ha un figlio sinistro il predecessore è l’antenato più vicino che contiene il nodo in questione nel suo sottoalbero destro ( non esiste se e solo se il nodo è il minimo assoluto) X == Y.left? NO: return Y(8) X=18 Y= X.up =8 X == Y.left? X=15 Y= X.up =18 X == Y.left? Pred(9) X=9 Y= X.up =15 Successore Successore di un dato nodo: cerca il nodo con chiave minore tra tutti i nodi con chiave strettamente maggiore di quella del nodo dato Due i casi da analizzare 1) Il nodo ha un figlio destro il successore è il minimo del sottoalbero destro. Il minimo non ha un figlio sinistro. 2) Il nodo non ha un figlio destro il successore è l’antenato più vicino che contiene il nodo in questione nel suo sottoalbero sinistro ( non esiste se e solo se il nodo è il massimo assoluto) function SearchSuccessor(T) if T = NIL then return NIL else if T.right= NIL then return SearchMin(T.right) else x = T y = x.up while y != NULL and x = y.right do x = y y = x.right end while return y end if end if end function • Quanto costa cercare il successore di un nodo? Caso pessimo: O(Height[T]). Attraversiamo tutti i nodi da foglia a radice. • Quanto costa cercare successore di un nodo senza puntatore al padre? Caso pessimo: O(Height[T]). Attraversiamo tutti i nodi da radice a foglia. Cancellazione di un nodo Vogliamo rimuovere un nodo dall’albero mantenendo le proprietà di un albero binario di ricerca. Tre i casi possibili il nodo da rimuovere può 1) Essere una foglia 2) Avere un solo figlio 3) Avere due figli 1) Il nodo da rimuovere è una foglia: il nodo viene rimosso 2) Il nodo da rimuovere ha un solo figlio: rimuoviamo il nodo e lo sostituiamo col figlio 3) Il nodo da rimuovere ha due figli: sostituiamo la chiave del nodo con la chiave del suo successore e rimuoviamo il successore. Il successore si trova nel sottoalbero a destra. La rimozione del successore ricade in uno dei due casi precedenti ( 1 o 2 ) DelNode(T) if IsLeaf(T) then DelKey(T; key) if IsLeftChild(T) then x <- SearchKey(T; key) Left[Up[T]] <- NULL if x != NIL then else if IsRightChild(T) then DelNode(x) Right[Up[T]] <- NULL end if end if /*cerco la chiave, se Delete(T) la trovo, elimino il else if HasOneChild(T) then nodo */ tmp <- Child(T) if IsLeftChild(T) then Left[Up[T]] <- tmp else if IsRightChild(T) then Right[Up[T]] <- tmp end if Up[tmp] <- Up[T] Delete(T) else if HasTwoChildren(T) then tmp <- SearchSuccessor(T) Key[T] <Key[tmp] DelNode(tmp) end if Cancellazione di un nodo – Costo computazionale • Quando costa DelNode? • I due casi, rimozione di una figlio o di un nodo con un figlio unico, vengono gestiti in modo costante. • Il caso di rimozione di un nodo con due figli costa quando la ricerca del successore SearchSuccessor • Caso pessimo: O(Height[T]). Attraversiamo tutti i nodi da foglia a radice. • Quando costa DelKey? • Richiama prima la funzione SearchKey e poi DelNode: • Costo pessimo di SearchKey: O(Height[T]). • Costo pessimo di DelNode : O(Height[T]). • Caso pessimo di DelKey: O(Height[T]) + O(Height[T]) = O(Height[T]). Implementare la funzione per la ricerca di un elemento, del massimo e del minimo all’interno di un albero binario di ricerca [ Esercizio proposto 5.1 ] Fornire un algoritmo, e implementare la soluzione, ( con il costo computazionale più basso nel caso pessimo) che dati in input un albero binario di ricerca T e due numeri interi positivi a e b , con a <= b ritorni il numero di elementi compresi all'intervallo [a, b] Esempio: Intervallo : [ 6,15 ] Output: 4 [ Esame 5.2 ] Scrivere una funzione che restituisca il predecessore di un elemento in un albero binario di ricerca. Usare tale funzione per ordinare l’albero intero. [ Esame 13/07/2011 ] Come cambia l’esercizio dell’esame 13/07/2011 per ottenere un array ordinato in modo crescente usando il predecessore? E se al posto del predecessore fosse richiesto di utilizzare il successore? Definire un algoritmo per determinare se un albero binario T in input è un albero binario di ricerca. Dati due alberi binari di ricerca T1 e T2 tali che le chiavi in T1 sono tutte minori delle chiavi in T2, scrivere una procedura che restituisce un albero di ricerca contenente tutte le chiavi in tempo O(h) [ Esercizi aggiuntivi 5.2 ] Domande e discussione Domande?