Riepilogo
Una funzione può chiamare sé stessa: la ricorsione risolve un problema riducendolo a un’istanza più piccola dello stesso problema. Questa lezione traduce in C++ definizioni ricorsive della matematica (il fattoriale, la potenza, la somma delle cifre), ciascuna con il suo caso base e il suo caso ricorsivo; mostra che l’induzione ne garantisce la correttezza e che una misura che decresce ne garantisce la terminazione; segue con lo stack delle chiamate i frame che si impilano e si disfano, e spiega quando lo stack si esaurisce. Esamina il limite del tipo del risultato, che va in overflow molto prima dello stack; confronta la versione ricorsiva e quella iterativa di Fibonacci contando le chiamate, e si chiude con un criterio per scegliere tra la ricorsione e un ciclo.
Obiettivi di apprendimento
- Scrivere una funzione ricorsiva, con il suo caso base e il suo caso ricorsivo, a partire da una definizione ricorsiva.
- Giustificare la correttezza e la terminazione di una funzione ricorsiva per induzione sulla dimensione del problema.
- Seguire con lo stack delle chiamate l’esecuzione di una funzione ricorsiva e spiegare l’overflow dello stack.
- Confrontare il costo della versione ricorsiva e di quella iterativa di Fibonacci contando le loro chiamate e le loro operazioni.
Definizioni ricorsive
Il fattoriale
Il fattoriale di un naturale è il prodotto dei naturali da 1 a : , con la convenzione . I puntini di sospensione di quella formula nascondono un ciclo, e la lezione Cicli ha insegnato a scriverlo. Ora, la stessa definizione ammette un’altra forma, che non menziona alcun ciclo: il fattoriale di è volte il fattoriale di , e quello di 0 è 1.
Questa è una definizione ricorsiva: definisce il fattoriale di per mezzo del fattoriale di un numero minore. Non è circolare, perché ogni applicazione della seconda regola scende di un gradino e la scala termina nella prima: . Le due regole hanno un nome. La prima è il caso base, che dà il risultato senza riferirsi ad altro; la seconda è il caso ricorsivo, che riduce il problema a un’istanza più piccola dello stesso problema.
C++ permette a una funzione di chiamare sé stessa, e con ciò la definizione si traduce quasi parola per parola.
#include <print>// Fattoriale di n.// Precondizione: 0 <= n <= 12 (il limite si giustifica più avanti).// Postcondizione: restituisce n! = 1 · 2 · ... · n, con 0! = 1.int factorial(int n){ if (n == 0) { return 1; // caso base } return n * factorial(n - 1); // caso ricorsivo}int main(){ for (int n{0}; n <= 6; ++n) { std::println("{}! = {}", n, factorial(n)); }}0! = 11! = 12! = 23! = 64! = 245! = 1206! = 720La funzione ha esattamente la forma della definizione: un if che tratta il caso base e un return che applica il caso ricorsivo. La chiamata factorial(n - 1) non è diversa da qualsiasi altra chiamata della lezione Funzioni: si valuta il suo argomento, si crea un nuovo frame con il suo n e, al termine, il valore restituito viene moltiplicato per l’n di chi ha chiamato.
Il contratto non è decorativo: con il caso ricorsivo chiamerebbe con , poi con , e non raggiungerebbe mai lo 0; e non sta in un int. Molti testi introduttivi scrivono questo fattoriale con il caso base if (n == 0 || n == 1), ridondante ma innocuo, e senza dire nulla dei negativi né della grandezza del risultato: il loro codice non termina con e dà un risultato privo di senso a partire da . Entrambi i difetti saranno esaminati nella loro sezione.
Caso base e caso ricorsivo
Ogni funzione ricorsiva ha uno o più casi base, che si risolvono direttamente, e uno o più casi ricorsivi, che chiamano la stessa funzione con un argomento minore in un senso preciso e combinano ciò che essa restituisce.
La potenza , con naturale, si definisce con e : la dimensione è l’esponente. La somma delle cifre di un naturale (quella di 2026 è ) si definisce così: se , la somma è , perché ha una sola cifra; altrimenti, è la somma delle cifre di , che è senza la sua ultima cifra, più quest’ultima cifra, . Qui la dimensione è il numero di cifre.
#include <print>// x elevato alla n.// Precondizione: n >= 0.// Postcondizione: restituisce x^n, con x^0 = 1.double power(double x, int n){ if (n == 0) { return 1.0; } return x * power(x, n - 1);}// Somma delle cifre decimali di n.// Precondizione: n >= 0.// Postcondizione: restituisce la somma delle cifre di n.int digitSum(int n){ if (n < 10) { return n; } return digitSum(n / 10) + n % 10;}int main(){ std::println("2^10 = {}", power(2.0, 10)); std::println("1.5^3 = {}", power(1.5, 3)); std::println("somma delle cifre di 2026: {}", digitSum(2026));}2^10 = 10241.5^3 = 3.375somma delle cifre di 2026: 10In digitSum, la divisione intera n / 10 toglie l’ultima cifra e n % 10 la estrae, come nel cambiamento di base della lezione Algoritmi numerici. Per , la funzione chiede la somma delle cifre di 202, che chiede quella di 20, che chiede quella di 2; questa è un caso base e restituisce 2, e i risultati si compongono al ritorno: , , .
Scrivere una funzione ricorsiva consiste, dunque, nel rispondere a due domande: quali casi si risolvono senza aiuto, e come si ottiene la soluzione di un caso a partire da quella di uno minore. Se entrambe le risposte sono corrette, lo è anche la funzione, e la sezione seguente lo dimostra.
Caricamento del contenuto…