Récursivité 
La récursivité est la possibilité pour une fonction de s'appeller elle-même. En général, il y a deux étapes pour écrire une fonction récursive :
- Un cas de base où la fonction renvoie directement une valeur.
- Un cas général où la fonction s'appelle sur des paramètres "plus petits".
En OCaml, une fonction récursive doit être définie par let rec .... Voici un exemple :
let rec f x = (* exemple de fonction récursive *)
if x = 0 then print_newline () (* cas de base *)
else (print_int x;
f (x - 1)) (* cas général *)
val f : int -> unit = <fun>
f x affiche un retour à la ligne si x est égal à 0, et sinon affiche x puis appelle f (x - 1).
Essayons cette fonction :
f 2
21
- : unit = ()
Voici ce qui se passe lors de cet appel f 2 :
- On regarde si
2 = 0, ce qui est faux. On passe donc dans leelse. - On affiche
2avecprint_int x. - On appelle
fsur la valeur 1. Le calcul def 2se met en pause et on exécutef 1. Quandf 1sera terminé, l'appel def 2continuera etf 1sera remplacé par sa valeur de retour. - L'exécution de
f 1affiche1puis appellef 0. Le calcul def 1se met en pause et on exécutef 0. Quandf 0sera terminé, l'appel def 2continuera etf 0sera remplacé par sa valeur de retour. f 0exécuteprint_newline ()et s'arrête (en renvoyant()).- L'exécution de
f 1reprend etf 1s'arrête. - L'exécution de
f 2reprend etf 2s'arrête.
Vous pouvez visualiser l'exécution d'un code similaire en Python avec Python Tutor. Il est important de bien comprendre comment les appels récursifs s'effectuent.
Un exemple classique d'utilisation de la récursivité est le calcul de la factorielle d'un entier $n$, définie par $n! = n \times (n - 1) \times ... \times 2 \times 1$.
Pour définir une fonction récursive calculant $n!$ on a besoin de deux choses :
- Cas de base : si $n = 0$, on peut renvoyer directement 1 (par convention $0! = 1$), sans appel récursif.
- Cas général/récurrence : si $n$ est quelconque, il faut rammener le calcul de $n!$ à un calcul d'une factorielle plus petite (de façon à se rapprocher du cas de base). Pour cela, on peut remarquer que $n! = n\times (n-1)!$ et donc que calculer $(n-1)!$ permet d'en déduire $n!$.
On en déduit le code suivant :
let rec fact n =
if n = 0 then 1 (* par convention 0! = 1 *)
else n*fact (n - 1)
val fact : int -> int = <fun>
fact 4
- : int = 24
Remarque : Si on oublie le cas de base (if n = 0) la fonction ne s'arrête jamais (fact 0 appelerait fact (-1) qui appelerait fact (-2) et ainsi de suite...) !
Écrire une fonction récursive ressemble beaucoup à écrire une démonstration mathématiques par récurrence et d'ailleurs on utilisera souvent une démonstration par récurrence pour démontrer qu'une fonction récursive est correcte, c'est à dire renvoie bien la bonne valeur.
Par exemple, pour démontrer que fact est correcte, on peut poser l'hypothèse de récurrence :
$$\mathcal{H}(n) : \text{fact } n \text{ renvoie } n!$$
Preuve :
- $\mathcal{H}(0)$ est vraie car
fact 0renvoie 1 et, par définition, $0! = 1$. - Soit $n$ un entier strictement positif. Supposons $\mathcal{H}(n - 1)$ et montrons $\mathcal{H}(n)$.
Comme $n > 0$,fact nrenvoien*fact (n - 1). D'après $\mathcal{H}(n - 1)$,fact (n - 1)renvoie $(n - 1)!$. Doncfact nrenvoie $n(n - 1)! = n!$, ce qui démontre $\mathcal{H}(n)$.
D'après le principe de récurrence, $\mathcal{H}(n)$ est vraie pour tout $n \in \mathbb{N}$.
Exercice
Qu'affiche le code suivant? Le deviner puis exécuter le code pour vérifier.
let rec f x =
if x = 0 then print_newline ()
else (f (x - 1);
print_int x) in
f 5
Solution
let rec f x =
if x = 0 then print_newline ()
else (f (x - 1);
print_int x) in
f 5
12345
- : unit = ()
Exercice
- Écrire une fonction récursive pour calculer la somme des $n$ premiers entiers $S(n) = 0 + 1 + 2 + ... + (n - 1)$.
- Quelle formule connaissez-vous pour calculer $S(n)$? En déduire une autre fonction (non récursive) pour calculer cette valeur. Vérifier sur des exemples que les deux fonctions donnent la même valeur.
Solution
(* 1. *)
let rec somme n =
if n = 1 then 1 (* S(1) = 1 *)
else n + somme (n - 1);; (* on utilise S(n) = n + S(n - 1) *)
(* 2. *)
let somme2 n =
n*(n+1)/2;; (* on utilise la formule de Gauss *)
somme 42 = somme2 42 (* vérification *)
val somme : int -> int = <fun>
val somme2 : int -> int = <fun>
- : bool = true
Une application classique de la récursivité est le calcul des termes d'une suite récurrente.
Par exemple :
$$\begin{cases}
u_{n} = 3u_{n-1} + 2, \text{si } n > 0\\
u_0 = 5
\end{cases}$$
Cette définition par récurrence se traduit naturellement en fonction récursive :
let rec u n =
if n = 0 then 5
else 3*(u (n - 1)) + 2
val u : int -> int = <fun>
u 10
- : int = 354293
Exercice
Calculer $v_{10}$, où $v_n$ est définie par $$\begin{cases} v_{n+1} = \sqrt{v_{n}} + 4, \text{si } n > 0\\ v_1 = 5 \end{cases}$$
Solution
let rec v n =
if n = 1 then 5.
else (v (n - 1))**0.5 +. 4. in (* le résultat est un flottant à cause de la racine *)
v 10
- : float = 6.56155211605287
Un autre exemple classique est la suite de Fibonacci : $$u_0 = 1$$ $$u_1 = 1$$ $$u_n = u_{n - 1} + u_{n - 2}$$
On pourrait l'implémenter de la façon suivante :
let rec fibo n =
if n <= 1 then 1
else fibo (n - 1) + fibo (n - 2) in
fibo 10
- : int = 89
Attention : cette méthode est très inefficace. Pour s'en convaincre, regardons visuellement les appels récursifs de fibo :
Il y a de nombreux calculs inutiles : par exemple, fibo 3 est appelé 2 fois et fibo 2 est appelé 3 fois, ce qui est inefficace.
Exercice
Montrer que le nombre d'appels récursifs pour calculer fibo n est exponentiel en $n$ (c'est à dire supérieur à $a^n$ pour un certain $a$ indépendant de $n$).
Solution
Soit $C_{n}$ le nombre d'appels récursifs effectués par fibo n (on compte l'appel de fibo n ainsi que tous ses sous-appels récursifs). Alors :
$$C_{n} = \underbrace{C_{n-1}}_{\text{appels récursifs de fibo (n-1)}} + \underbrace{C_{n-2}}_{\text{appels récursifs de fibo (n-2)}}$$
$C_{n}$ vérifie donc la même équation de récurrence que la suite de Fibonacci. Pour donner un minorant simplement, on peut remarquer que $C_{n-1} \geq C_{n - 2}$ (car $C_{n-1} = C_{n - 2} + \underbrace{C_{n - 3}}_{\geq 0}$) donc $C_{n} \geq 2 C_{n - 2}$ et en appliquant cette inégalité plusieurs fois :
$$C_{n} \geq 2 C_{n - 2} \geq 2^2 C_{n - 4} \geq 2^3 C_{n - 6} \geq ... \geq 2^\frac{n}{2} C_{n - 2\frac{n}{2}} = 2^\frac{n}{2}$$
On a donc montré que $\boxed{\forall n \in \mathbb{N}, C_{n} \geq 2^\frac{n}{2}}$ : fibo n effectue un nombre exponentiel (en $n$) d'appels récursifs.
Il est possible d'éviter ces appels inutiles en utilisant un accumulateur. Un accumulateur est un argument d'une fonction récursive que l'on va utiliser pour construire le résultat final. L'accumulateur est modifié à chaque appel récursif.
let rec fibo2 n a b =
(* n est le nombre de termes restants à calculer *)
(* a est le dernier terme calculé de la suite *)
(* b est l'avant-dernier terme calculé *)
if n = 0 then b
else fibo2 (n - 1) (a + b) a in (* les derniers termes deviennent a+b et a *)
fibo2 10 1 1
- : int = 89
On verra aussi plus tard une technique de mémoïsation permettant d'éviter de faire 2x le même appel récursif, de façon systématique.
Bien sûr, on pourrait aussi utiliser une boucle for en stockant les deux derniers termes de la suite dans des variables, mais l'objectif ici est de s'entraîner à penser récursivement.
Sous-fonction récursive¶
Quand on souhaite écrire une fonction f x en utilisant une méthode récursive mais que x doit être accessible dans les appels récursifs, on peut utiliser une sous-fonction récursive dans f, et f se contentera d'appeler cette fonction.
Par exemple, pour savoir si un entier est premier :
let premier n =
let rec f k = (* renvoie true si n n'a pas de diviseurs entre 2 et k *)
if k = 1 then true (* on a regardé tous les diviseurs potentiels *)
else if n mod k = 0 then false (* si k divise n *)
else f (k - 1) in (* vérifie que n n'a pas de diviseurs entre 2 et k *)
f (n - 1) (* teste si n a un diviseur entre 2 et n - 1 *)
val premier : int -> bool = <fun>
premier 2 && premier 3 && not (premier 4) (* test *)
- : bool = true