Fondamenti di Informatica II Ingegneria Informatica Prof. M.T. PAZIENZA a.a. 2003-2004 – 3° ciclo Ricorsione Per ricorsione si intende la tecnica di risoluzione di un problema in termini della soluzione ad una versione più piccola (situazione fondamentale) dello stesso problema. Caratteristiche della ricorsione: • ciascun passo ricorsivo semplifica la risoluzione generale del problema • l’ultimo passo (passo base) del problema viene risolto direttamente • Identificazione di una condizione di uscita dalla ricorsione (coincide con il caso base) Ricorsione Punti su cui riflettere: • Concetto di ricorsione • Relazioni tra ricorsione ed iterazione • Quali problemi devono essere risolti con metodi ricorsivi e quali con metodi iterativi? • Ricorsione come approccio mentale • Scrivere funzioni ricorsive aiuta la comprensione delle stesse • Capire quando usare la ricorsione determina l’efficienza di un algoritmo Risorsione Approccio: • Identifica qual è l’aspetto fondamentale del problema • Risolvilo • Struttura il problema più complesso in termini di quello fondamentale • Problema generale come una cascata di problemi minori ciascuno risolto dalla stessa funzione che affronta sottoproblemi sempre più piccoli finché raggiunge quello fondamentale e lo elabora senza problemi. • L’importante è identificare la struttura base dell’algoritmo Ricorsione (esempio1) Permutazione Si consideri una funzione che produca tutte le permutazione di una stringa. Esempio di permutazioni di una stringa di lettere: "eat" "eta" "aet" "ate" "tea" "tae" Se una stringa ha n lettere, allora il numero delle permutazioni è dato dalla funzione fattoriale: n! = 1 x 2 x 3 x . . . x n Ovvero n! = (n - 1)! x n Ricorsione (esempio1) Per calcolare il valore di n! si può usare un ciclo, oppure ricorrere alla soluzione ricorsiva n! = (n - 1)! x n con l’assunzione fondamentale (passo base) che: 1! = 1 0! = 1 Quindi si può implementare una funzione fattoriale del tipo: int factorial(int n) { if (n == 0) return 1; int smaller_factorial = factorial(n-1); int result = smaller_factorial * n; return result; } Ricorsione (esempio1) Una funzione che generi tutte le permutazioni di una parola è data da vector<string> generate_permutations(string word); Tutte le permutazioni della stringa "eat“ sono date da: vector<string> v= generate_permutations("eat"); for(int i = 0; i < v.size(); i++) cout << v[i] << "\n"; Ricorsione (esempio1) Per generare tutte le permutazioni ricorsivamente, si generano tutte le permutazioni che cominciano con la lettera 'e', poi quelle che cominciano con la lettera a, quindi quelle che cominciano con la lettera t. Oppure applichiamo la permutazione (con un approccio ricorsivo) su stringhe da due ottenendo "at" "ta“, "et" "te“, "ae" "ea" Aggiungiamo le prime lettere (lasciate inizialmente da parte) per trovare tutte le permutazioni "eat" "eta“, "aet" "ate“, "tae" "tea" Ricorsione (esempio2) Una parola si dice palindroma se è costituita da una stringa che è uguale e se stessa quando la si consideri dal verso opposto (es. anna, alla, ici, …) Step 1: Semplificare l’input in modo che lo stesso metodo risolutivo possa essere applicato alla componente più semplice del problema; esempi: Eliminare il primo carattere Eliminare l’ultimo carattere Eliminare insieme il primo e l’ultimo carattere Eliminare un carattere dal centro della parola Tagliare la stringa in due metà. Ricorsione (esempio2) Step 2: Si combinano le soluzioni relative agli input più semplici con la soluzione del problema originale La complessità di ciascuna soluzione parziale non deve preoccupare, viene lasciata al passo successivo (che sarà risolto da qualcun altro) • Si consideri l’ipotesi di tagliare a metà la parola: nel caso di “rotor” non si ottengono risultati incoraggianti • Si consideri l’ipotesi (più interessante) di eliminare il primo e l’ultimo carattere : con la parola “rotor” si ottiene "oto“. Verificata che è palindroma, sarebbe verificata che anche la stringa iniziale è palindroma, quindi (definiz. ricorsiva) Una parola è palindroma se il primo e l’ultimo carattere coincidono, (passo base) la parola ottenuta eliminando il primo e l’ultimo carattere è palindroma Ricorsione (esempio2) Step 3: Trovare la soluzione per il caso base (più semplice). La ricorsione si ferma con il caso base Nel caso delle parole palindrome il caso base coincide con: • stringhe di due caratteri • stringhe con un solo carattere • stringa vuota (senza caratteri) La stringa di due caratteri non richiede una elaborazione speciale: basta toglierle il primo e l’ultimo carattere e diventa una stringa vuota. 1. Una stringa di un solo carattere è palindroma per definizione 2. Una stringa vuota è palindroma per definizione Ricorsione (esempio2) Step 4: La soluzione finale si ottiene combinando la soluzione del caso semplice con quella del caso base Talvolta risulta più facile trovare soluzioni ricorsive se si cambia leggermente il problema originario Talvolta capita che funzioni cooperanti si chiamino vicendevolmente in un approccio ricorsivo (ricorsione reciproca) (esempio: calcolo delle espressioni aritmetiche) Ricorsione (esempio3) Ricorsione reciproca Per una espressione aritmetica si può affermare che: • Un’espressione è un termine, o una somma di termini, o una differenza di termini • Un termine è o un fattore, o un prodotto di fattori, o un quoziente di fattori • Un fattore è o un numero (caso base) o una espressione racchiusa tra parentesi Efficienza della ricorsione Sebbene la ricorsione costituisca un approccio molto potente per la risoluzione di algoritmi complessi, l’efficienza delle implementazioni può non essere ottimale richiedendo un tempo incredibile per arrivare alla soluzione finale Molto spesso la soluzione iterativa e quella ricorsiva hanno la stessa prestazione; solo pochi problemi hanno una soluzione ricorsiva più veloce di quella iterativa. Le soluzioni ricorsive risultano più facili da capire ed implementare correttamente Le soluzioni ricorsive sono molto più efficienti nell’interfaccia utente e richiedono uno sforzo di analisi più che di programmazione