> Quelques cours > Algorithmique et Analyse > 6. Récursion
| version imprimable |
6. Récursion
On dit qu'une procédure est récursive lorsque dans le corps de cette procédure il y a une appel à cette procédure elle-même (récursion directe) ou un appel à une autre procédure qui à son tour appelle la première (récursion indirecte).
|
La célèbre tête de vache dans le médaillon porte aux oreilles deux pendentifs, dans chacun figure une tête de vache qui porte aux oreilles deux pendentifs où figure ... Ce procédé, connu sous le nom de "mise en abyme", a été utilisé par certains peintres et écrivains, il constitue un exemple flagrant de processus récursif. |
![]() |
L'exemple le plus connu est la fonction "factorielle". la première fois qu'on la rencontre en mathématique, elle est engendrée par n! = 1.2.3.4...n (n étant un entier naturel) ; par la suite, on préfère une définition plus élégante1 n! = n.(n-1) qui n'a de sens que si elle est assortie de 0! = 1. En voici la traduction en notations algorithmiques :
1. La question est de savoir si la notion de récursion est plus ou moins naturelle que la notion d'itération ne relève pas de notre propos ; l'expérience semble seulement nous prouver que nombre d'étudiants rencontrent "certaines difficultés" lors de son étude.
fonction factorielle (n)
si n
1 alors
factorielle
1
sinon
factorielle
n * factorielle (n-1)
fin_si
fin_fonction factorielle
Il faut, lorsqu'on veut écrire une procédure récursive, avoir l'assurance que le nombre d'appels récursifs sera fini (tout comme le nombre d'étapes d'itération lors de l'écriture d'une boucle!). Sans vouloir s'intéresser aux cas trop pathologiques on peut dire, en simplifiant, que l'écriture d'une procédure récursive nécessite que, lors des appels successifs, le domaine des arguments aille en retrécissant, convergeant vers une limite qui corresponde au test d'arrêt des appels récursifs.
Sur un plan pratique, pour que le code engendré puisse être récursif il doit, pour chaque appel de la procédure :
Un tel fonctionnement peut facilement être réalisé sur une machine à pile2 où les zones de travail de chaque procédure sont allouées en "pile" (cf. le "stack frame" évoque en 2.2), une seule zone étant utilisée à la fois.
2. On illustre dans l'Appendice 6.5 comment se réalisent les appels de procédure sur la machine SPARC étudiée par ailleurs.
6.3. Exemple : traversée d'un arbre
Nous rappelons, en la développant, la procédure esquissée au chapitre précédent : en particulier, nous créons des variables locales p, f_g, f_d (bien qu'elles ne soient pas strictement nécessaires) afin de mieux pouvoir illustrer, lors des appels, le stockage des variables locales.
procédure parcourir_rameau (p_rameau : pointeur sur t_noeud, Entrée;
/* pointe sur la racine */ )
/* parcours "infixe" version récursive */
p, f_g, f_d : pointeurs sur t_noeud, variables locales;
p
p_rameau
f_g
f_gauche (p);
f_d
f_droit (p);
si f_g
NIL alors
parcourir_rameau (f_g) prochaine action à faire en revenant du rameau f_g : "imprimer" la clé
fin_si
imprimer clé(p)
si f_d
NIL alors
parcourir_rameau (f_d) par contre, en revenant du rameau f_d, il n'y a rien d'autre à faire que de terminer la procédure, donc : "return"
fin_si
fin_procédure parcourir_rameau
On va simuler le fonctionnement de cette procédure et examiner au cours du temps le déroulement des appels et les valeurs des jeux de variables pour l'arbre ci-dessous.

Dans le schéma qui suit, on a représenté :
Lors de chaque appel à parcourir_rameau, la valeur de l'argument d'appel a été encadrée.

Simulation de l'exécution récursive (allocation de "stack frames")
6.4. Méthode de "dérécursification"
Nous allons nous intéresser à la transformation d'algorithmes récursifs en algorithmes itératifs.
Il existe des algorithmes permettant d'automatiser cette transformation.
On peut aussi réaliser la transformation d'un algorithme récursif en un algorithme itératif en examinant les "actions" effectuées par le processeur lors de l'éxecution de l'algorithme récursif et en simulant ce fonctionnement à l'aide d'un automate très simple (à un seul état). Les principales difficultés sont rassemblées dans la simulation de la séquence d'appel de la procédure, il faut :
En examinant le texte de la procédure récursive, on dresse la liste des différentes actions réalisées (elles constituent, en quelque sorte, le jeu d'instructions exécutables par le processeur dont on va simuler le fonctionnement) :
procédure parcourir_rameau (p_rameau : pointeur sur t_noeud, Entrée
/* pointe sur la racine */ )
/* on suppose p_rameau
NIL */
p, f_g, f_d : pointeurs sur t_noeud, variables locales;
|
p f_g f_d |
action = débuter |
|
si f_g parcourir_rameau(f_g) fin_si |
action = appeler "parcourir rameau" |
| imprimer clé(p) | action = imprimer |
|
si f_d parcourir_rameau(f_d) fin_si |
action = appeler "parcourir rameau" |
| fin_procédure parcourir_rameau | action = terminer |
Procédure récursive : étude des actions
Formalisation :
Type
ty_action = (débuter, appeler, imprimer, terminer)
fin_type ty_action
Au cours de l'exécution de chaque action, on détermine l'action qui devra être exécutée "juste après" (c'est ce que nous appelerons "action courante" A_C).
Voici le squelette du "moteur d'automate" qui simule un processeur :
variable locale
A_C : ty_action /* "action courante" c.-à-d. à exécuter immédiatement */
| répéter | ||
| si | A_C = débuter | alors |
| ... | ||
| sinon si | A_C = imprimer | alors |
| ... | ||
| sinon si | A_C = appeler | alors |
| ... | ||
| sinon si | A_C = terminer | alors |
| ... | ||
| fin_répéter | ||
Moteur d'automate
6.4.3. Simulation de la séquence d'appel
Dans la réalité le programme appelant détermine, avant chaque appel, les valeurs des arguments d'appel ; la procédure appelée récupère les arguments d'appel et réalise le traitement prévu.
Notre procédure (dérécursifiée) simule l'exécution d'une procédure qui s'appelle elle-même ; on est donc appelé à simuler, à la fois, ce qui se passe à l'extérieur de la procédure (séquence d'appel) et à l'intérieur (récupération des arguments). Dans le cas étudié ici, la procédure n'a qu'un seul argument, un pointeur vers la racine du sous-arbre à parcourir : nous avons appelé p_call la valeur de ce pointeur établie au moment de l'appel ("à l'extérieur" de la procédure) ; nous avons appelé p l'argument correspondant manipulé "à l'intérieur" de la procédure. Donc avant chaque appel simulé il faut déterminer la valeur de p_call et, lors du début de l'exécution correspondante, il faut recopier la valeur de p_call dans la variable p "interne" à la procédure simulée.
Lorsque, dans la réalité, l'instruction exécutée est un appel de procédure, le processeur stocke (au moment du saut) l'adresse de l'instruction du programme appelant qui sera exécutée immédiatement après la fin de la procédure appelée. Nous allons simuler ce mécanisme en stockant avant chaque appel simulé, la "prochaine action" (c.-à-d. l'action qui doit être exécutée lorsque la procédure dont on simule l'appel aura fini de s'exécuter) dans la variable P_A.
p_call : pointeur vers ty_noeud, variable locale, simule l'argument d'appel
P_A : ty_action, /* action à exécuter après que A_C a été achevée */
simule le stockage de la valeur du compteur ordinal au moment de l'appel
Variables locales utilisées au passage entre appels simulés
6.4.4. Simulation du "stack-frame"
C'est une structure de pile qu'on implémente dans un tableau d'agrégats. Chaque élément de la pile contiendra un environnement c.-à-d. les variables locales et la "prochaine action". Cette structure de pile est gérée par des procédures calquées sur les prototypes du chapitre 2 : Empiler, Dépiler, Init_Pile.
Type ty_environnement = agrégat
p, f_g, f_d : pointeurs sur ty_noeud;
P_A : ty_action
fin_agrégat ty_environnement
Type ty_pile_de_récursion = agrégat
t_environnement : tableau de ty_environnement indice[1..prof_max];
prof : indice pour t_environnement
fin_agrégat ty_pile_de_récursion
Définition de l'environnement et de la pile
En supposant que tout a été initialisé, on peut écrire chaque action :
action débuter
p
p _call /* suppose p_call
NIL */
f_g
f_gauche(p)
f_d
f_droit(p)
p_call
f_g
P_A
imprimer
A_C
appeler
action imprimer
imprimer clé(p)
p_call
f_d
P_A
terminer
A_C
appeler
action appeler
si p_call
NIL alors
Empiler (p, f_g, f_d, P_A) ce n'est pas la syntaxe complète
A_C
débuter
sinon
A_C
P_A
fin_si
action terminer
variable locale OK : booléen /* rend compte de la bonne exécution de "Dépiler" */
Dépiler (p, f_g, f_d, P_A, OK)
si OK alors
A_C
P_A
sinon la pile était vide, ce qui signifie qu'on a fini de traiter tous les appels
return donc quitter la procédure parcourir_rameau
fin_si
6.4.6. Procédure itérative Parcourir_rameau
Assemblons et complétons tout ce qui précède :
procédure parcourir_rameau (p_rameau : pointeur sur ty_noeud, Entrée
/* version itérative */ /* pointe sur la racine */
Types
ty_action = (débuter, appeler, imprimer, terminer)
fin_type ty_action
ty_environnement = agrégat
p, f_g, f_d : pointeurs sur ty_noeud;
P_A : ty_action
fin_type ty_environnement
ty_pile_de_récursion = agrégat
t_environnement : tableau de ty_environnement indice[1..prof_max];
prof : indice pour t_environnement
fin_type ty_pile_de_récursion
variables locales
environnement_courant : ty_environnement; /* p, f_g, f_d, P_A */
/* P_A simule le stockage de la valeur du compteur ordinal au moment de l'appel */
stack_frame : ty_pile_de_récursion
A_C : ty_action /* "action courante" c.-à-d. à exécuter immédiatement */
p_call : pointeur vers ty_noeud, variable local, simule l'argument d'appel
OK : booléen /* rend compte de la bonne exécution de "Dépiler" */
on travaille avec environnement_courant
si p_rameau
NIL alors
Init_Pile (stack_frame)
p_call
p_rameau
A_C
débuter
répéter
si A_C = débuter alors
p
p_call
f_g
f_gauche(p)
f_d
f_droit(p)
p_call
f_g
P_A
imprimer
A_C
appeler /* fin_cas A_C = débuter */
sinon si A_C = imprimer alors
imprimer clé(p)
p_call
f_d
P_A
terminer
A_C
appeler /* fin_cas A_C = imprimer */
sinon si A_C = appeler alors
si p_call
NIL alors
Empiler (environnement_courant) syntaxe!
A_C
débuter
sinon
A_C
P_A
fin_si /* fin_cas A_C = appeler */
sinon si A_C = terminer alors
Dépiler (environnement_courant, OK)
si OK alors
A_C
P_A
sinon la pile est vide, ce qui signifie qu'on a fini de traiter tous les appels
return quitter la boucle et la procédure
fin_si /* fin_cas A_C = terminer */
fin_si /* choix des actions */
fin_répéter
fin_si /* p_rameau
NIL */
fin_procédure parcourir_rameau
procédure itérative Parcourir_rameau
6.5. Procédure récursive ou itérative
La question que l'on pose souvent est celle-là.
Oublions l'apsect historique : pendant longtemps les langages dominants (FORTRAN, COBOL) n'ont pas permis la récursion. Au moins en ce qui concerne FORTRAN, cette époque est révolue3 : la norme 90 permet la récursion et nombre de compilateurs FORTRAN 77 actuels supportent en tant qu'extension par rapport à la norme.
3. Cependant dans les traités et les bibliothèques de programme, il existe encore de nombreux exemples de programmes et d'algorithmes dont la nature récursive a été masquée par l'emploi de tableaux destinés à simuler un fonctionnement de pile, sans que cette dérécursification soit clairement énoncée.
Nous proposerons quelques règles simples :
4. Un parcours d'arbre peut avoir lieu sans qu'il y ait explicitement de structures de données de type arbre, par exemple une recherche dichotomique, ou encore une recherche avec des marches arrière ("back tracking").
Il faut noter que :
5. Si le problème le permet, il peut être avantageux de rejeter en appel récursif terminal la récursion qui aurait entraîné le plus d'appels récursifs (nous verrons plus loin un exemple dans la procédure "QuickSort").
6.6. Appendice : appel de procédure et gestion de la mémoire

Diagramme illustrant l'activation succesive des différentes unités d'un programme
Les flèches indiquent les changements de procédure active et les instructions concernées, les différentes colonnes correspondent au déroulement du temps.
N.B. Le code du programme n'existe qu'à un seul exemplaire et les duplications des codes dans les boîtes ci-dessus n'ont été faites que pour faciliter l'identification des éléments des procédures concernées (le code complet : programme plus sous-programme) figure dans la colonne de gauche.

Allocation des "stack frames" propres à chaque procédure,
(chaque stack frame contient entre autres, les variables locales à la procédure correspondante)
N.B. La pile croît vers le bas ; en grisé les éléments de pile temporairement inaccessibles, en hachuré la zone mémoire inutilisée.

Fenêtre des registres "visibles" par la procédure active
(diagramme spécifique à une machine SPARC, le contenu des registres correspond au cas du programme FORTRAN)
On a supposé que, à l'entrée dans chaque procédure, l'instruction SAVE est exécutée et que RESTORE est exécutée à la sortie ; les valeurs des registres %O sont celles au moment de l'appel de la procédure fille ; le caractère @ est l'abréviation usuelle de "adresse de".
On a montré, en regard du déroulement d'un programme, les allocations des "stack frame" dans lesquels sont implantées les variables locales.
Dans le cas de FORTRAN on a rappelé, en outre, comment étaient utilisés les registres pour communiquer les arguments et mémoriser les paramètres caractéristiques de l'exécution de chaque procédure.

Diagramme illustrant l'activation successive des différentes unités d'un programme
Les flèches indiquent les changements de procédure active et les instructions concernées, les différentes colonnes correspondent au déroulement du temps.
N.B. Le code du programme n'existe qu'à un seul exemplaire et les duplications des codes dans les boîtes ci-dessus n'ont été faites que pour faciliter l'identification des éléments des procédures concernées (le code complet : programme et sous-programme) figure dans la colonne de gauche.

Allocation des "stack frames" propres à chaque procédure,
(chaque stack frame contient entre autres, les variables locales à la procédure correspondante)
N.B. La pile croît vers le bas ; en grisé les éléments de pile temporairement inaccessibles, en hachuré la zone mémoire inutilisée.
| retour vers le haut |