Aller au contenu
Topos Uranos

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

  1. Écrire une fonction récursive, avec son cas de base et son cas récursif, à partir d’une définition récursive.
  2. Justifier la correction et la terminaison d’une fonction récursive par récurrence sur la taille du problème.
  3. Suivre avec la pile d’appels l’exécution d’une fonction récursive et expliquer le débordement de pile.
  4. 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 nn est le produit des entiers naturels de 1 à nn : n!=1⋅2⋯nn! = 1 \cdot 2 \cdots n, avec la convention 0!=10! = 1. 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 nn est nn fois la factorielle de n−1n - 1, et celle de 0 vaut 1.

0!=1,n!=n⋅(n−1)!(n≥1).0! = 1, \qquad n! = n \cdot (n - 1)! \quad (n \geq 1).

C’est une définition récursive : elle définit la factorielle de nn 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 : 3!=3⋅2!=3⋅2⋅1!=3⋅2⋅1⋅0!=63! = 3 \cdot 2! = 3 \cdot 2 \cdot 1! = 3 \cdot 2 \cdot 1 \cdot 0! = 6. 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.

ExempleLa factorielle récursive
Programme en C++
#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));    }}
Sortie
0! = 11! = 12! = 23! = 64! = 245! = 1206! = 720

La 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 n=−1n = -1, le cas récursif appellerait avec −2-2, puis avec −3-3, et n’atteindrait jamais 0 ; et 13!13! 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 n<0n < 0 et donne un résultat dénué de sens à partir de 13!13!. 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 xnx^n, avec nn naturel, se définit par x0=1x^0 = 1 et xn=x⋅xn−1x^n = x \cdot x^{n - 1} : la taille est l’exposant. La somme des chiffres d’un entier naturel nn (celle de 2026 est 2+0+2+6=102 + 0 + 2 + 6 = 10) se définit ainsi : si n<10n < 10, la somme est nn, car il n’a qu’un chiffre ; sinon, c’est la somme des chiffres de ⌊n/10⌋\lfloor n / 10 \rfloor, qui est nn privé de son dernier chiffre, plus ce dernier chiffre, n mod 10n \bmod 10. Ici, la taille est le nombre de chiffres.

ExempleLa puissance et la somme des chiffres
Programme en C++
#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));}
Sortie
2^10 = 10241.5^3 = 3.375somme des chiffres de 2026 : 10

Dans 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 n=2026n = 2026, 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 : 2+0=22 + 0 = 2, 2+2=42 + 2 = 4, 4+6=104 + 6 = 10.

É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.