Ripasso di programmazione ricorsiva Ripasso di programmazione ricorsiva Algoritmo ricorsivo: algoritmo espresso in termini di se stesso. Programmazione iterativa e programmazione ricorsiva sono equivalenti. Vantaggi/svantaggi rispetto a programmazione iterativa: Ricorsione è meno efficiente, ma è più semplice ed elegante per risolvere alcuni problemi (divide-et-impera, alberi) Condizioni necessarie per la correttezza: presenza di uno o più casi base convergenza verso i casi base La programmazione iterativa è in generale più efficiente della programmazione ricorsiva, perché l'invocazione di una funzione è un'operazione poco efficiente. Tuttavia, la programmazione ricorsiva risulta più semplice nel caso di algoritmi divide-et-impera e di algoritmi che operano su strutture dati gerarchiche, come gli alberi binari e gli alberi generici. Programmare in modo iterativo in questi casi è possibile ma produrrebbe codice complesso e poco comprensibile.
Ripasso di programmazione ricorsiva Relazioni di ricorrenza divide-et-impera T n = d T n = hn k at n b con n n con n n T n O n k T n O n k log n T n O n log ba sea b k sea=b k sea b k
Albero binario Definizione: Un insieme vuoto di nodi è un albero binario Un nodo p, più un albero binario B s (sottoalbero sinistro) e un albero binario B d (sottoalbero destro) è un albero binario
Classe IntBinTree IntBinTree realizza un semplice albero binario di interi ( IntBinTree.h, IntBinTree.cpp ) Ogni elemento è rappresentato da una struttura Node, che contiene: L'etichetta (int info), Il puntatore all'elemento di sinistra (Node* left), Il puntatore all'elemento di destra (Node* right) All'interno della classe è definito il puntatore all'elemento radice (Node* root)
Classe IntBinTree Per il debugging, sono definiti gli operatori put to (<<) e get from (>>) È possibile usarli per immettere un albero binario da tastiera e stamparlo a video IntBinTree mio_binario; cin >> mio_binario; // acquisisce da tastiera /* Elaborazioni varie... */ cout << mio_binario; // stampa a video
Classe IntBinTree Formato di input/output: 10 20 30 Gli spazi sono ignorati [10 [20] [30] ] Se il figlio destro non c'è: [10 [20] ] Se il figlio sinistro non c'è: [10 [] [30] ]
Classe IntBinTree Se l'albero ha più di un livello, il formato è ricorsivo: 10 20 30 20 34 100 44 [10 [20[20]] [30[34[][44]][100]] ]
Programmazione ricorsiva a oggetti class IntBinTree: public BaseBinTree<int>{ /* */ public: void esercizio(){ // esercizio(); // sottoalbero sinistro? esercizio(); // sottoalbero destro? } }; È necessario applicare il seguente accorgimento per programmare in modo ricorsivo su classi. Per esportare la funzionalità (e per testare il programma dal main) è necessario scrivere un metodo pubblico della classe IntBinTree. Però l'esercizio non può essere svolto in questo metodo pubblico, perché, non avendo un puntatore al nodo come argomento, non può essere richiamato ricorsivamente.
Programmazione ricorsiva a oggetti 1 2 3 4 6 7 8 9 10 11 12 class IntBinTree{ /* */ private: static void _esercizio(node* r){ // _esercizio(r->left); _esercizio(r->right); } public: void esercizio(){_esercizio(root);} // funzione di interfaccia }; Quindi il metodo pubblico deve invocare un altro metodo privato (con un nome simile) e passargli il puntatore a root come argomento esplicito. Il puntatore this che viene passato sempre come argomento implicito risulta così inutile. Per aumentare l'efficienza e la leggibilità, si elimina dichiarando statico il metodo privato.
File per l'esercitazione Copiare dalla cartella condivisa in una cartella locale: BaseBinTree.h ( da non modificare) IntBinTree.h IntBinTree.cpp Includere i file nel progetto Dev-Cpp
o 1 Aggiungere a IntBinTree una funzione ricorsiva che restituisce la somma di tutte le etichette pari dell'albero. 10 8 20 = 40 2
o 1 Soluzione 1 2 3 4 6 7 int IntBinTree::_sumEven(const Node* t){ if (t == NULL) return 0; } return (t->info % 2 == 0?t->info:0) + _sumeven(t->left) + _sumeven(t->right); T 0 = d T n = h 2T n 2 T n O n
o 2 Aggiungere a IntBinTree una funzione ricorsiva che specchia l'albero. 10 10 20 20 2 8 8 2 [ 10 [20] [[2[]][8]] ] [ 10 [[8][2[][]]] [20] ]
o 2 Soluzione 1 2 3 4 6 7 8 9 void IntBinTree::_mirror(Node* t){ if (t == NULL) return; // scambio il sottoalbero sinistro con il destro: Node* app = t->left; t->left = t->right; t->right = app; _mirror(t->left); _mirror(t->right); } T (0) = d T (n) = h+ 2T( n 2) T (n) Ο(n)
o 3 Aggiungere a IntBinTree una funzione che conta quanti nodi hanno un numero pari di discendenti 10 3 2 = 4 8
o 3 Soluzione 1 2 3 4 6 7 8 9 10 11 12 int IntBinTree::conta(const Node* r){ if(!r) return 0; return 1 + conta(r->left) + conta(r->right); } int IntBinTree::_esercizio3(const Node* r){ if(!r) return 0; int numdisc = conta(r->left) + conta(r->right); return (numdisc % 2 == 0? 1 : 0) + _esercizio3(r->left) + _esercizio3(r->right); }
o 3 Soluzione conta(): O n _esercizio3(): T (0) = d T (n) = hn+ 2T( n 2) O nlog n
Tecnica per diminuire la complessità Si fa una funzione sola invece di due La funzione esegue due cose contemporaneamente: Conta i discendenti Conta i nodi che hanno un numero pari di discendenti La funzione deve restituire due interi Il secondo intero viene restituito tramite un riferimento a intero int _esercizio3(const Node* r, int& numdisc); return: Numero di nodi che hanno un numero pari di discendenti return: Numero dei discendenti
o 3 Soluzione efficiente 1 2 3 4 6 7 8 9 10 11 int IntBinTree::_esercizio3(const Node* r, int& numnodi){ if(!r) { numnodi = 0; return 0; } } int numnodil, numnodir; int countl = _esercizio3(r->left, numnodil); int countr = _esercizio3(r->right, numnodir); numnodi = numnodil + numnodir + 1; return ((numnodil + numnodir) % 2 == 0? 1 : 0) + countl + countr; T (0) = d T (n) = h+2t( n 2) Ο(n)
Programmazione ricorsiva a oggetti 1 2 3 4 class IntBinTree{ /* */ public: int esercizio3(){ int numnodi; return _esercizio3(root, numnodi); } }; La funzione di interfaccia deve avere una variabile locale per accogliere il secondo valore di ritorno, che poi viene scartato.
o 4 Aggiungere a IntBinTree una funzione ricorsiva che conta quanti nodi hanno un nel sottoalbero destro ma non nel sinistro 10 3 8 2 = 2
o 4 Soluzione 1 2 3 4 6 7 8 9 10 11 12 13 14 1 16 bool isinside(int i, const Node* r){ if(!r) return false; if(r->info == i) return true; if(isinside(i, r->left)) return true; return isinside(i, r->right); } int conta(const Node* tree){ if(!tree) return 0; bool cinques = isinside(, tree->left); bool cinqued = isinside(, tree->right); int cs = conta(tree->left); int cd = conta(tree->right); if(cinqued &&!cinques) return cs + cd + 1; else return cs + cd; } O nlog n
o 4 Soluzione efficiente 1 2 3 4 6 7 8 9 10 11 int conta(const Node* tree, bool& cinque){ if (!tree) {cinque = false; return 0;} } bool cinques, cinqued; int c = conta(tree->left, cinques) + conta(tree->right, cinqued); cinque = cinques cinqued (tree->label == ); if(cinqued &&!cinques) return c+1; else return c; O n