Au début du xiii-ème siècle, le mathématicien italien Léonard de Pise surnommé Fibonacci, a proposé de décrire l'évolution d'une population de lapins (dont tout le monde sait qu'ils se reproduisent comme des…). L'hypothèse simplificatrice est que chaque mois deux lapins adultes mâle et femelle donnent naissance à 2 lapereaux mâle et femelle et qu'il faut un mois à un lapereau pour devenir adulte et pouvoir procréer. La question est de savoir combien de couples de lapins (ou lapereaux) il y aura dans le clapier après \(n\) mois ? On suppose que le clapier contient initialement 2 lapereaux.
À l'étape 0, il y a donc un seul couple de lapereaux. Après un mois, ces lapins sont adultes. Après deux mois ce couple de lapins donne naissance à leur premier couple de lapereaux, il y a donc 2 couples, etc.
En mathématiques, la suite de Fibonacci \((F_n)_{n\in{\N}}\) est une suite d'entiers naturels dont le terme courant est égal à la somme des deux termes précédents, les deux premiers termes de la suite étant \(F_0=0\) et \(F_1=1\). On peut exprimer cette définition à l'aide de la récurrence :
\begin{equation}\label{eq:fibo} {\color{#DDA}F_n}= \begin{cases} 1&\text{si}\ n\in\{0,1\},\\ F_{n-1}+F_{n-2}&\text{sinon.} \end{cases} \end{equation}ALGORITHME Fibonacci(n):entier
DONNEES
n: entier
VARIABLES
F,x,y,i: entiers
DEBUT
SI n < 2 ALORS
RENVOYER 1
SINON
x ← 1
y ← 1
i ← 2
TQ i ≤ n FAIRE
F ← x + y
x ← y
y ← F
FTQ
RENVOYER F
FSI
FIN
De la même manière que l'on peut définir une suite ou une fonction récurrente en mathématiques, on peut définir un algorithme récursif en informatique. Le parallèle est parfaitement adapté puisqu'une fonction récursive s'adapte en général immédiatement en son équivalent algorithmique récursif.
L'algorithme précédent s'écrit récursivement :
ALGORITHME Fibonacci(n):entier
DONNEES
n: entier
DEBUT
SI n < 2 ALORS
RENVOYER 1
SINON
RENVOYER Fibonacci(n-1) + Fibonacci(n-2)
FSI
FIN
L'algorithme est uniquement constitué par les deux blocs de l'instruction conditionnelle SI / ALORS / SINON. Le premier bloc (instruction #6) constitue ce que l'on appelle la base récurrente qui correspond à la situation que l'algorithme sait traiter sans faire appel à lui-même. C'est l'homoloque informatique de l'initialisation dans un raisonnement par récurrence en mathématiques. La seconde appelée autodéfinition concerne la partie de l'algorithme qui fait référence à lui même, ici un unique appel récursif (instruction #8). C'est l'homoloque informatique de l'hérédité dans un raisonnement par récurrence en mathématiques.
L'idée sous-jacente, tout comme pour une récurrence, est que le ou les appels récursifs se font sur des instances plus petites du ou des paramètres de l'algorithme, ici \(n-1\) et \(n-2\) au lieu de \(n\). Comme l'algorithme sait traiter le cas de base, la suite des appels récursifs sur des instances de plus en plus petites aboutira inexorablement à un appel sur la ou les instances de la base récurrente ce qui achèvera le processus. Retenons donc les deux concepts clefs d'un algorithme récursif : la base récurrente et l'autodéfinition.
La plupart des langages de programmation modernes autorisent les définitions récursives des fonctions et même la récursivité croisée, où une fonction \(A\) fait appel à une fonction \(B\) qui appelle \(A\). Cette liberté d'écriture offerte au programmeur repose sur l'existence d'une pile. À chaque appel de la fonction, un bloc d'activation contenant les paramètres d'appel de la fonction, ses variables locales et sa valeur de retour est empilé. La fonction travaille toujours avec les variables du bloc d'activation au sommet de la pile. Si elle est appelée à nouveau récursivement, un nouveau bloc d'activation est empilé et la fonction travaillera avec les variables de ce nouveau bloc et ainsi de suite jusqu'à ce que sa valeur de retour soit instanciée et qu'elle termine son exécution, moment où le bloc d'activation est dépilé.
Ainsi, pour l'algorithme récursif Fibonacci, l'algorithme opère sur une nouvelle variable \(n\) et une nouvelle valeur de retour placées au sommet d'une pile à chaque nouvel appel. La figure ci-dessous contient l'arbre des appels récursifs de l'algorithme Fibonacci pour la valeur \(n=4\). La valeur de retour de l'algorithme est l'étiquette des branches.
La table suivante décrit l'état de la pile au fur et à mesure des appels récursif de l'algorithme Fibonacci pour les valeurs \(n\) inférieures ou égales à 5 (afin de limiter la taille de l'affichage). Dans la pile, le bloc d'activation contient la valeur \(n\) et la valeur de retour ret de l'algorithme (matérialisée par un point tant qu'elle n'a pas été instanciée).
Mais, ce qui distingue fondamentalement une méthode récursive d'une méthode itérative, c'est la nécessité ou non de conserver tous les calculs intermédiaires, autrement dit la nécessité de disposer d'une pile ou non. On utilise donc une pile si l'on a affaire mathématiquement à une récurrence forte ou une récurrence faible. Dans le premier cas, on a besoin de tous les \(P(k)\) pour \(k\leqslant n\) pour obtenir \(P(n+1)\), dans le second on n'a besoin que de l'étape précédente \(P(n)\).
Ainsi le calcul du \(n\)-ème terme de la suite de Fibonacci n'est pas un problème de nature récursive. En effet, seuls deux termes nécessitent d'être conservés à chaque étape pour pouvoir calculer le prochain, il est donc artificiel et dispandieux de conserver toutes les valeurs \(F_i\) pour \(i\lt n\). L'algorithme itératif Fibonacci initialement proposé est à privilégier.
Il est également important de noter que dans la version récursive, outre l'empilement superflu d'une quantité non négligeable d'informations, certains calculs sont effectués plusieurs fois inutilement comme le montre l'arbre des appels récursifs dans la figure 1 avec les deux nœuds \(F(2)\) qui par construction sont la racine du même sous-arbre. C'est un écueil majeur et très commun lorsqu'on écrit des algorithmes récursifs.
Si ni le calcul de la suite de Fibonacci, ni le calcul de la factorielle d'un entier ne sont des problèmes de nature récursive, malgré ce que suggèrent leurs définitions récurrentes, quel type de problème justifie une écriture récursive ?
Nous allons aborder un problème très classique en informatique qui a le mérite d'illustrer la puissance de l'écriture récursive en fournissant une solution simple et particulièrement élégante. La résolution de ce problème demanderait des efforts beaucoup plus conséquents s'il fallait se limiter à une écriture itérative classique pour calquer la méthode manuelle. Sans une analyse plus approfondie, il faudrait ni plus ni moins recréer une pile calquant ce que fait la pile d'exécution dans la version récursive.
Le problème des tours de Hanoï, imaginé par l'arithméticien français Edouard Lucas à la fin du xix-ème siècle, consiste à déplacer les disques d'une pile de \(n\) disques de diamètres décroissants de leur tour d'origine vers une autre tour en s'aidant d'une troisième tour auxiliaire. Il faut respecter les deux conditions suivantes :
Dans la suite les tours seront numérotées de 1 à 3 de la gauche vers la droite et nous coderons \(x\rightarrow y\) le déplacement du disque au sommet de la pile de la tour \(x\) vers le sommet de la pile de la tour \(y\). Bien entendu ce déplacement est supposé respecter la règle 2. Par convention la pile est initialement placée sur la première tour et il faut la déplacer vers la troisième tour.
Le problème est trivial pour \(n=1\), une solution évidente est \(1\rightarrow 3\). Pour \(n=2\) c'est à peine plus compliqué, la suite des déplacements \(1\rightarrow 2,\ 1\rightarrow 3,\ 2\rightarrow 3\) constitue une solution. Pour \(n=3\) cela demande un peu plus de réflexion : \[1\rightarrow 3,\ 1\rightarrow 2,\ 3\rightarrow 2,\ 1\rightarrow 3,\ 2\rightarrow 1,\ 2\rightarrow 3,\ 1\rightarrow 3.\] Notons déjà qu'il existe plusieurs solutions pour une instance de taille \(n\) et qu'il est naturel de s'interroger sur l'optimalité des solutions en termes de nombre de déplacements à réaliser. Il est aisé de prouver que les solutions fournies pour \(n=1\) et \(n=2\) ci-dessus sont optimales. Nous étudierons cette question dans l'étude de la complexité de l'algorithme qui résout ce problème.
Analysons le problème. Notons \(A\), \(B\) et \(C\) les trois tours. Pour déplacer \(n\) disques de la tour \(A\) à la tour \(C\), il faut déplacer \(n-1\) disques de la tour \(A\) à la tour \(B\) puis déplacer le dernier disque de la tour \(A\) à la tour \(C\) et finalement déplacer les \(n-1\) disques de la tour \(B\) à la tour \(C\). Autrement dit si l'opération Déplacer(n,A,C) signifie déplacer \(n\) disques de la tour \(A\) vers la tour \(C\), alors elle peut se décomposer en trois opérations plus simples :
ALGORITHME Déplacer(n,X,Y)
DONNEES
n,X,L: entiers
DEBUT
SI (n = 1)
Afficher(X, "→", Y)
SINON
Déplacer(n-1, X, 6-X-Y)
Afficher(X, "→", Y)
Déplacer(n-1, 6-X-Y, Y)
FSI
FIN
Les paramètres \(X\) et \(Y\) de l'algorithme désignent respectivement la tour d'origine et la tour de destination des \(n\) disques.