1 FONDAMENTI DI INFORMATICA II Ingegneria Gestionale a.a ° Ciclo Alberi
2 Definizione Un albero è una struttura dati non lineare perché i suoi nodi contengono due o più membri di link. Sono alberi binari quelli che contengono in ogni nodo esattamente due membri di link. Elementi di un albero binario Il nodo radice è il primo nodo di un albero, generalmente puntato da un puntatore esterno alla struttura dell’albero. I link del nodo radice, che convenzionalmente indicheremo come link di sinistra o link di destra, possono puntare a due altri nodi, detti nodi figli, o assumere il valore 0. Se esistono i nodi figli essi possono considerarsi come primi nodi rispettivamente del sottoalbero di sinistra e del sottoalbero di destra. I suddetti nodi figli possono a loro volta puntare ad altri nodi figli e ciascuno di essi può essere considerato il primo di un sottoalbero. Un nodo senza figli, ossia con entrambi i link a l valore 0, viene detto nodo foglia.
3 Alberi Rappresentazione grafica di un albero A D CB Nodo radice Nodo foglia Puntatore esterno al nodo radice Sottoalbero di destra Nodo figlio
4 Alberi Rappresentazione grafica semplificata di un albero Nella rappresentazione grafica semplificata di un albero vengono presentati, senza particolari riquadrature, solo i dati contenuti nei nodi e, per mezzo di segmenti, i link tra i nodi. Un link mancante corrisponde nel nodo ad un puntatore di valore 0. Il nodo radice viene presentato in alto e, per convenzione i link sono diretti dall’alto verso il basso. L’esempio che segue è quello di un albero con un valore intero per nodo:
5 Alberi Albero binario di ricerca Un albero binario di ricerca è caratterizzato dal fatto che, dato un qualunque nodo, i valori dei dati contenuti nel sottoalbero di sinistra sono inferiori al valore del dato presente nel nodo in esame, che, a sua volta, è minore dei valori dei dati contenuti nel sottoalbero di destra. Nota: La forma di un albero di ricerca che corrisponde ad un insieme di dati non è unica: essa può variare secondo l’ordine in cui i valori sono inseriti nell’albero.
6 Alberi Esempio di albero binario di ricerca
7 Alberi Funzioni ricorsive Si dicono funzioni ricorsive quelle funzioni che richiamano se stesse. Quando una funzione ricorsiva invoca se stessa la sua elaborazione s’interrompe a quel punto e il controllo viene ceduto ad una nuova istanza della funzione, che, a sua volta, può richiamare se stessa e quindi cedere il controllo ad una nuova istanza, …ecc. Se ad un certo punto un’ultima istanza della funzione riesce a completare i calcoli senza invocare se stessa essa può far ritornare il controllo all’istanza che l’ha invocata, la quale può completare i calcoli e restituire a sua volta il controllo alla precedente istanza e così via fino alla prima. Una funzione ricorsiva deve avere quindi uno schema di operazioni del tipo descritto con lo schema a blocchi che segue.
8 Alberi No Funz_Ricors (parametro) Parametro rela - tivo al caso base Richiamo Funz_Ricors con parametro tendente al caso base Elaborazioni Si Elaborazioni del caso base return
9 Alberi Esempio di funzione ricorsiva Funzione per il calcolo del fattoriale di un numero: int factorial (int number) { if (number= =1)return 1; else return number * factorial (number-1); } Si può dare una rappresentazione grafica del funzionamento di una funzione ricorsiva indicando con una freccia il momento del suo richiamo ricorsivo, con un cerchietto, contenente il valore del parametro, la sua istanziazione e con un arco accompagnato dal valore del risultato il momento della restituzione del controllo. Nel caso della funzione factorial a cui sia stato passato il parametro number= 4, questa rappresentazione è: *13*24*6
10 Alberi // Classe TreeNode per la generazione dei nodi di un albero #ifndef TREENODE_H #define TREENODE_H template class Tree; // forward declaration template class TreeNode { friend class Tree ; public: TreeNode( const NODETYPE &d ) : leftPtr( 0 ), data( d ), rightPtr( 0 ) { } NODETYPE getData() const { return data; } private: TreeNode *leftPtr; // pointer to left subtree TreeNode *rightPtr; // pointer to right subtree NODETYPE data;}; #endif
11 Alberi // Definition of template class Tree #ifndef TREE_H #define TREE_H #include #include "treenode.h" using std::endl; template class Tree { public: Tree( ); void insertNode( const NODETYPE & ); void preOrderTraversal( ) const; void inOrderTraversal( ) const; void postOrderTraversal( ) const; private: TreeNode *rootPtr;
12 Alberi // utility functions void insertNodeHelper( TreeNode **, const NODETYPE & ); void preOrderHelper( TreeNode * ) const; void inOrderHelper( TreeNode * ) const; void postOrderHelper( TreeNode * ) const; }; template Tree ::Tree( ) { rootPtr = 0; } template void Tree ::insertNode( const NODETYPE & value ) { insertNodeHelper( &rootPtr, value ); }
13 Alberi // This function receives a pointer to a pointer so the // pointer can be modified. template void Tree ::insertNodeHelper( TreeNode **ptr, const NODETYPE &value ) { if ( *ptr == 0 ) { // tree (or sub tree) is empty *ptr = new TreeNode ( value ); assert( *ptr != 0 ); } else // tree is not empty if ( value data ) insertNodeHelper( &( ( *ptr )->leftPtr ), value ); else if ( value > ( *ptr )->data ) insertNodeHelper( &( ( *ptr )->rightPtr ), value ); else cout << value << " dup" << endl;}
14 Alberi template void Tree ::preOrderTraversal() const { preOrderHelper( rootPtr ); } template void Tree ::preOrderHelper( TreeNode *ptr ) const { if ( ptr != 0 ) { cout data << ' '; preOrderHelper( ptr->leftPtr ); preOrderHelper( ptr->rightPtr ); }
15 Alberi template void Tree ::inOrderTraversal() const { inOrderHelper( rootPtr ); } template void Tree ::inOrderHelper( TreeNode *ptr ) const {if ( ptr != 0 ) { inOrderHelper( ptr->leftPtr ); cout data << ' '; inOrderHelper( ptr->rightPtr ); }
16 Alberi template void Tree ::postOrderTraversal() const { postOrderHelper( rootPtr ); } template void Tree ::postOrderHelper( TreeNode *ptr ) const { if ( ptr != 0 ) { postOrderHelper( ptr->leftPtr ); postOrderHelper( ptr->rightPtr ); cout data << ' '; } #endif
17 Alberi // Driver to test class Tree #include #include "tree.h" using std::cout; using std::cin; using std::setiosflags; using std::ios; using std::setprecision; int main() { Tree intTree; int intVal, i; cout << "Enter 10 integer values:\n"; for( i = 0; i < 10; i++ ) { cin >> intVal; intTree.insertNode( intVal );}
18 Alberi cout << "\nPreorder traversal\n"; intTree.preOrderTraversal(); cout << "\nInorder traversal\n"; intTree.inOrderTraversal(); cout << "\nPostorder traversal\n"; intTree.postOrderTraversal(); Tree doubleTree; double doubleVal; cout << "\n\n\nEnter 10 double values:\n" << setiosflags( ios::fixed | ios::showpoint ) << setprecision( 1 ); for ( i = 0; i < 10; i++ ) { cin >> doubleVal; doubleTree.insertNode( doubleVal );}
19 Alberi cout << "\nPreorder traversal\n"; doubleTree.preOrderTraversal(); cout << "\nInorder traversal\n"; doubleTree.inOrderTraversal(); cout << "\nPostorder traversal\n"; doubleTree.postOrderTraversal(); return 0; }
20 Alberi Enter 10 integer values: Preorder traversal Inorder traversal Postorder traversal Enter 10 double values: Preorder traversal Inorder traversal Postorder traversal
21 Alberi Rappresentazione grafica dell’albero ottenuto con i 10 valori interi
22 Alberi Visite ricorsive Il grafico che segue rappresenta le visite ricorsive dei rami dell’albero effettuate con le funzioni preOrderTraversal, inOrderTraversal e postOrderTraversal. Ogni nodo è rappresentato con un cerchietto contenente il dato (intero) in esso immagazzinato. All’uscita da ciascun nodo la visita al ramo di sinistra è rappresentata da una freccia verticale, mentre la visita al ramo di destra da una freccia inclinata. Il ritorno dopo una visita è rappresentato con un arco. Nelle funzioni le visite ai rami di sinistra precedono sempre quelle dei rami di destra. Nelle tre funzioni le elaborazioni del dato del nodo sono eseguite come segue: preOrder: prima della prima uscita dal nodo. inOrder: prima della seconda uscita dal nodo. postOrder: dopo il rientro definitivo nel nodo
23 Alberi
24 Alberi Importanza degli alberi binari di ricerca In un albero di ricerca ben bilanciato ogni livello conterrà un numero di elementi pari a circa il doppio degli elementi del livello precedente. Gli elementi di ogni livello crescono perciò con le potenze di 2 per cui al livello k gli elementi saranno 2 k -1. Dato un albero con n elementi su k livelli essi saranno: n= k -1 = 2 k -1 per cui approssimativamente il numero di livelli di un albero di ricerca ben bilanciato è pari a log 2 n. Ciò vuol dire che con 1000 elementi l’albero ha circa 10 livelli (infatti 2 10 >1000), ossia una ricerca su 1000 elementi non richiede più di 10 confronti.