Résumé
Une fonction peut s’appeler elle-même : la récursivité résout un problème en le ramenant à une instance plus petite du même problème. Cette leçon traduit en C++ des définitions récursives de la mathématique (la factorielle, la puissance, la somme des chiffres), chacune avec son cas de base et son cas récursif ; elle montre que la récurrence garantit leur correction et qu’une mesure qui décroît garantit leur terminaison ; elle suit avec la pile d’appels les cadres qui s’empilent et se défont, et explique quand la pile s’épuise. Elle examine la limite du type du résultat, qui déborde bien avant la pile ; elle compare la version récursive et la version itérative de Fibonacci en comptant les appels, et elle se termine par un critère pour choisir entre la récursivité et une boucle.
Objectifs d’apprentissage
- Écrire une fonction récursive, avec son cas de base et son cas récursif, à partir d’une définition récursive.
- Justifier la correction et la terminaison d’une fonction récursive par récurrence sur la taille du problème.
- Suivre avec la pile d’appels l’exécution d’une fonction récursive et expliquer le débordement de pile.
- Comparer le coût de la version récursive et de la version itérative de Fibonacci en comptant leurs appels et leurs opérations.
Définitions récursives
La factorielle
La factorielle d’un entier naturel est le produit des entiers naturels de 1 à : , avec la convention . Les points de suspension de cette formule cachent une boucle, et la leçon Boucles a appris à l’écrire. Or la même définition admet une autre forme, qui ne mentionne aucune boucle : la factorielle de est fois la factorielle de , et celle de 0 vaut 1.
C’est une définition récursive : elle définit la factorielle de au moyen de la factorielle d’un nombre plus petit. Elle n’est pas circulaire, car chaque application de la seconde règle descend d’une marche et l’escalier se termine sur la première : . Les deux règles ont un nom. La première est le cas de base, qui donne le résultat sans renvoyer à rien d’autre ; la seconde est le cas récursif, qui ramène le problème à une instance plus petite du même problème.
C++ permet à une fonction de s’appeler elle-même, et la définition se traduit alors presque mot pour mot.
#include <print>// Factorielle de n.// Précondition : 0 <= n <= 12 (la borne est justifiée plus loin).// Postcondition : renvoie n! = 1 · 2 · ... · n, avec 0! = 1.int factorial(int n){ if (n == 0) { return 1; // cas de base } return n * factorial(n - 1); // cas récursif}int main(){ for (int n{0}; n <= 6; ++n) { std::println("{}! = {}", n, factorial(n)); }}0! = 11! = 12! = 23! = 64! = 245! = 1206! = 720La fonction a exactement la forme de la définition : un if qui traite le cas de base et un return qui applique le cas récursif. L’appel factorial(n - 1) ne diffère en rien de n’importe quel autre appel de la leçon Fonctions : son argument est évalué, un nouveau cadre est créé avec son propre n et, à la fin, la valeur renvoyée est multipliée par le n de l’appelant.
Le contrat n’est pas décoratif : avec , le cas récursif appellerait avec , puis avec , et n’atteindrait jamais 0 ; et ne tient pas dans un int. Beaucoup de textes d’introduction écrivent cette factorielle avec le cas de base if (n == 0 || n == 1), redondant mais inoffensif, et sans rien dire des négatifs ni de la taille du résultat : leur code ne se termine pas avec et donne un résultat dénué de sens à partir de . Ces deux défauts seront examinés dans leur section.
Cas de base et cas récursif
Toute fonction récursive a un ou plusieurs cas de base, qui se résolvent directement, et un ou plusieurs cas récursifs, qui appellent la même fonction avec un argument plus petit en un sens précis et combinent ce qu’elle renvoie.
La puissance , avec naturel, se définit par et : la taille est l’exposant. La somme des chiffres d’un entier naturel (celle de 2026 est ) se définit ainsi : si , la somme est , car il n’a qu’un chiffre ; sinon, c’est la somme des chiffres de , qui est privé de son dernier chiffre, plus ce dernier chiffre, . Ici, la taille est le nombre de chiffres.
#include <print>// x élevé à la puissance n.// Précondition : n >= 0.// Postcondition : renvoie x^n, avec x^0 = 1.double power(double x, int n){ if (n == 0) { return 1.0; } return x * power(x, n - 1);}// Somme des chiffres décimaux de n.// Précondition : n >= 0.// Postcondition : renvoie la somme des chiffres de 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("somme des chiffres de 2026 : {}", digitSum(2026));}2^10 = 10241.5^3 = 3.375somme des chiffres de 2026 : 10Dans digitSum, la division entière n / 10 enlève le dernier chiffre et n % 10 l’extrait, comme dans le changement de base de la leçon Algorithmes numériques. Pour , la fonction demande la somme des chiffres de 202, qui demande celle de 20, qui demande celle de 2 ; celle-ci est un cas de base et renvoie 2, et les résultats se composent au retour : , , .
Écrire une fonction récursive consiste donc à répondre à deux questions : quels cas se résolvent sans aide, et comment on obtient la solution d’un cas à partir de celle d’un cas plus petit. Si les deux réponses sont correctes, la fonction l’est aussi, et la section suivante le démontre.
Chargement du contenu…