Académique Documents
Professionnel Documents
Culture Documents
Fonctions rcursives
Centre Informatique pour les Lettres
et les Sciences Humaines
Document du 10/02/06
C++ - Leon 15
Fonctions rcursives
2/9
Lorsqu'on la rencontre pour la premire fois, l'ide selon laquelle les instructions places
l'intrieur du corps d'une fonction pourraient comporter un appel la fonction elle-mme semble
souvent inepte. Comment une suite d'instructions pourrait-elle dcrire valablement la faon
d'excuter un traitement, si l'une de ces instructions exige l'application du traitement ? C'est un
peu comme si les instructions d'assemblage d'un meuble livr en kit comportaient une tape
enjoignant de... monter le meuble. En programmation, pourtant, ce phnomne est si frquent
qu'il porte un nom : les fonctions qui s'appellent elles-mme sont dites rcursives.
void devineResultatTierce(int & premier, int & second, int & troisieme)
{
devineResultatTierce(premier, second, troisieme); //appel rcursif ?
}
Cette fonction revient simplement dire que pour deviner le rsultat du prochain tierc, il
suffit de deviner le rsultat du prochain tierc. La nouvelle excution de la fonction dclenche
par l'appel rcursif n'est pas plus avance que la premire. Tout ce qu'elle peut faire, c'est
dclencher une troisime excution, et ainsi de suite, l'infini.
Ou, plus exactement, jusqu' ce que la mmoire disponible soit puise. Lors d'un appel de
fonction, il faut en effet que le processeur conserve une trace de l'tat d'avancement de la
fonction appelante, de faon pouvoir continuer lorsque l'excution de la fonction appele sera
acheve. Si minime que soit la quantit de mmoire ncessaire cette conservation, quelques
secondes suffiront certainement la fonction ci-dessus pour dclencher les millions d'excutions
embotes les unes dans les autres qui, en exigeant que l'tat d'avancement de chacune d'entre
elles soit conserv, finiront par puiser la mmoire disponible, quelle que soit la taille de celle-ci.
Pour que le programme ne sombre pas, victime d'une rcursion sans fin, il faut que l'appel
rcursif ne soit excut que conditionnellement.
Examinons par exemple la fonction suivante :
1
2
3
4
5
6
7
8
void penible()
{
char reponse;
cout << "Voulez-vous continuer vous enfoncer dans la rcursion ?\n";
cin >> reponse;
if(reponse != 'N')
penible();
//appel rcursif CONDITIONNEL !
}
Ce fragment de code, comme les autres exemples proposs dans cette Leon, suppose un
contexte d'entres/sorties "consoles" dans lequel l'insertion dans cout (4) provoque un affichage
l'cran alors que l'extraction depuis cin (5) lit le clavier. Ce contexte (un peu archaque, vrai
dire) prsente l'avantage de permettre aux fonctions de produire une trace de leur excution
sans qu'aucune mise en place et transmission de variables ne soit ncessaire.
Il y a, dans ce cas, fort parier que l'utilisateur finira par se lasser et par presser la touche
libratrice, bien avant que la mmoire disponible n'ait t puise par la rcursion. Que se
passe-t-il alors ? L'excution en cours ne procde aucun appel rcursif, mais s'achve
normalement. L'excution de la fonction appelante peut donc reprendre, ce qui signifie ici
qu'elle peut elle-mme s'achever. La rsolution de la situation se propage ainsi jusqu'au niveau
le plus lev, o l'achvement de l'excution signifie l'achvement de la rcursion.
Une faon image de se reprsenter la situation ralise lors de l'excution d'une rcursion est
de remplacer mentalement l'appel rcursif par une copie du code de la fonction.
Ds que les fonctions concernes cessent d'tre trs simples, une telle reprsentation ne peut
plus vraiment tre ralise dans tous ses dtails, mais son principe peut vous aider ne pas
perdre le fil, ou le retrouver lorsque vous l'aurez perdu...
Mme une fonction comportant un appel rcursif ne se mettra pas spontanment en route ! Il faut, comme toujours,
qu'une autre fonction fasse un appel (non rcursif, par dfinition) la fonction pour que celle-ci soit excute.
C++ - Leon 15
Fonctions rcursives
3/9
Voici ce que donnerait cette reprsentation dans le cas ou un utilisateur, confront notre
fonction penible(), rpondrait quatre fois 'O', avant de finir par dire 'N' :
{//CECI N'EST PAS UNE FONCTION, MAIS UNE REPRESENTATION D'UNE EXECUTION
char reponse;
cout << "Voulez-vous continuer vous enfoncer dans la rcursion ?\n";
cin >> reponse;
if(reponse != 'N')
{//il a rpondu O (le fou !)
char reponse;
cout << "Voulez-vous continuer vous enfoncer dans la rcursion ?\n";
cin >> reponse;
if(reponse != 'N')
{//il a rpondu O (le fou !)
char reponse;
cout << "Voulez-vous continuer vous enfoncer dans la rcursion ?\n";
cin >> reponse;
if(reponse != 'N')
{//il a rpondu O (le fou !)
cout << "Voulez-vous continuer vous enfoncer dans la rcursion ?\n";
cin >> reponse;
if(reponse != 'N')
{//il a rpondu O (le fou !)
cout << "Voulez-vous continuer vous enfoncer dans la rcursion ?\n";
cin >> reponse;
if(reponse != 'N')
// il a rpondu N (enfin !)
}
}
}
}
Cette reprsentation concrtise la notion d'enchssement des excutions : il est manifeste que
l'excution (bleue) du deuxime appel est l'intrieur de l'excution (noire) du premier, qui
reste en suspens et ne s'achvera que lorsque le deuxime appel se sera lui-mme achev.
Comme la fin de l'excution bleue dpend son tour de l'achvement de la rouge, qui lui-mme
exige la fin de la verte, qui n'est atteint que lorsque l'excution de la mauve est termine, on
voit bien que la rponse 'N' fournie lors de l'excution mauve provoque non seulement la fin de
cette excution mais, en cascade, celles de toutes les excutions de niveau suprieur.
Remarquons au passage que cette faon de reprsenter l'excution d'un appel de fonction peut
trs bien tre applique un appel "ordinaire" (i.e. non rcursif). Dans certains cas, il ne s'agit
d'ailleurs pas d'une simple vue de l'esprit, comme vous pourrez le dcouvrir en lisant
l'Annexe 6 : Fonctions inline.
Si la notion de fonction rcursive ne pose gure de problme2, la matrise des appels rcursifs
exige de bien comprendre les consquences de l'enchssement des excutions. Chacune de ses
excutions dispose en effet de son propre jeu d'objets locaux, et l'usage qui peut tre fait des
objets accessibles une fonction rcursive dpend donc fondamentalement de la rponse la
question suivante : toutes les excutions enchsses travaillent-elles sur les mmes objets ?
Il ne s'agit, aprs tout, qu'une fonction qui s'appelle elle-mme. D'un certain point de vue, cette dfinition ne justifie
sans doute pas une Leon complte : elle tient parfaitement dans une petite note de bas de page !
C++ - Leon 15
Fonctions rcursives
4/9
class CExemple
{
protected:
int compteur;
public:
CExemple(int val = -1) : compteur(val){}
void affiche();
};
Si la fonction affiche() est dfinie ainsi :
1
2
3
4
5
6
void CExemple::affiche()
{
cout << compteur << " ";
if (--compteur > 0)
affiche(); //appel rcursif ne spcifiant explicitement aucune instance
}
l'excution du code suivant
1
2
CExemple uneInstance(12);
uneInstance.affiche();
se traduira par l'affichage de
12 11 10 9 8 7 6 5 4 3 2 1
et par la remise zro du contenu de la variable uneInstance.compteur.
La fonction affiche() commence par afficher le contenu de la variable membre. Comme celle-ci
a t initialise 12, c'est cette valeur qui apparat. Lors de l'valuation de l'expression
contrlant l'appel rcursif conditionnel, la premire opration effectue est de dcrmenter le
compteur (puisque l'oprateur -- est ici prfix). La nouvelle valeur, 11, est ensuite compare 0
et, comme elle est suprieure, l'appel rcursif est effectu. Comme il s'agit de l'appel d'une
fonction membre et qu'aucune instance n'est spcifie, cet appel est automatiquement effectu
au titre de l'instance sur laquelle travaille la fonction appelante, c'est dire uneInstance.
L'excution enchsse ainsi dclenche dispose donc d'un compteur qui ne contient plus que 11,
et elle affiche cette valeur avant de procder son tour la dcrmentation du compteur et un
deuxime appel rcursif. Au cours du 11 appel rcursif, le compteur ne contient plus que 1.
Une fois cette valeur affiche, la dcrmentation conduit une valeur nulle, et cette excution
s'achve donc sans appel rcursif. Cet achvement permet au 10 appel rcursif de s'achever
son tour et, de proche en proche, tous les appels enchsss se terminent, y compris l'appel
initial (non rcursif). Le compteur, pour sa part, n'est plus affect et reste donc nul.
Le facteur dterminant est ici que l'appel rcursif exploite le privilge des fonctions membre qui
les autorise accder aux membres de leur classe sans prciser au titre de quelle instance
elles veulent le faire. L'usage implicite du pointeur cach this (cf. Leon 9) garantit que, par
dfaut, c'est l'instance au titre de laquelle la fonction a t appele qui sera utilise. Dans le
cas de notre appel rcursif, la fonction membre s'appelle donc elle-mme au titre de l'instance
qui a t utilise lors de l'appel initial (non rcursif).
Cet exemple illustre bien le fait que la variable membre tablit une communication
bidirectionnelle entre les diffrents niveaux d'excution :
- Chaque excution trouve dans la variable membre la valeur qu'y a plac l'excution du niveau
immdiatement suprieur (c'est ce qui permet la rcursion de s'achever). Ce type de
communication pourrait tre appele descendante.
- Lorsqu'une excution s'achve, celle qui l'a dclenche retrouve la variable membre dans
l'tat o l'a laisse l'excution qui vient de s'achever (c'est pour cela que la variable membre
contient finalement zro). Ce type de communication pourrait tre appele ascendante.
C++ - Leon 15
Fonctions rcursives
5/9
1
2
3
int n = 12;
CExemple uneInstance; //affiche() n'utilise plus la variable membre
uneInstance.affiche(n);
conduit au mme rsultat que celui obtenu prcdemment, c'est dire la mise zro du
contenu de la variable n et l'affichage suivant :
12 11 10 9 8 7 6 5 4 3 2 1
Bien entendu, le fait que la fonction rcursive soit ou non membre d'une classe ne change
absolument rien au fait que le passage d'une rfrence (ou d'un pointeur) rend l'objet concern
"commun" et tablit donc une communication bidirectionnelle entre les diffrents niveaux.
Remarquez que, dans cet exemple, la fonction ne fait aucun usage d'un autre membre de la
classe. Elle fonctionnerait donc exactement de la mme faon si elle tait globale, mis part,
videmment, le fait que l'appel initial ne devrait alors pas tre fait au titre d'uneInstance de
CExemple.
Variables locales
Par dfinition, les variables locales une fonction sont recres chaque fois qu'une excution
de la fonction est lance. Dans le cas d'un appel rcursif, chaque excution disposera donc de
son propre jeu de variables locales, ce qui signifie que ces variables ne peuvent pas tre
utilises pour contrler la rcursion ou, plus gnralement, pour tablir une communication
entre les diffrents niveaux d'enchssement3. En revanche, cette caractristique des variables
locales les protge contre les effets des appels enchsss, ce qui explique que, si la fonction
affiche() est prive de paramtre et dfinie ainsi :
Sauf, bien entendu, si l'appel rcursif transmet une rfrence l'une d'entre elles, ou un pointeur sur l'une d'entre
elles. Du point de vue de la rcursion, la variable locale concerne ressemble alors une variable membre implique
dans un appel rcursif effectu (implicitement) au titre de *this..
C++ - Leon 15
1
2
3
4
5
6
7
Fonctions rcursives
6/9
void CExemple::affiche()
{
int locale = compteur;
if (--compteur > 0)
affiche();
//appel rcursif
cout << locale << " ";
}
son appel par
1
2
Passage de valeurs
Lorsqu'une fonction reoit une simple valeur destine initialiser un de ses paramtres, ce
paramtre est un objet propre la fonction, exactement comme dans le cas d'une variable
locale. La version suivante de la fonction affiche() utilise cette proprit pour afficher deux
fois la valeur qui lui est transmise : une fois avant de procder l'appel rcursif, et une
seconde fois aprs.
1
2
3
4
5
6
7
8
9
10
void CExemple::affiche(int p)
{
cout << p << " ";
if (p > 1)
affiche(p-1); //appel rcursif
else
cout << "\nA la remonte : "; //on est arriv au fond !
cout << p << " ";
p = 0; //cette instruction n'a aucun effet
}
Comme la valeur du paramtre est parfaitement protge contre les effets des excutions
enchsses, c'est bien la mme valeur qui est affiche deux fois par chaque excution.
Toutefois, comme ces deux affichages sont spars par un appel rcursif, ils n'apparaissent
pas l'un aprs l'autre l'cran, mais sont spars par les affichages produits par les excutions
enchsses. Seule l'excution la plus profonde (qui est, par dfinition, dispense d'appel
rcursif) est en mesure d'afficher tout ce qu'elle a afficher sans tre interrompue. Elle en
profite (7) pour passer la ligne et annoncer le dbut de la remonte rcursive.
Si cette fonction est appele par
1
2
3
CExemple uneInstance;
//affiche() n'utilise plus la variable membre
cout << "A la descente : ";
uneInstance.affiche(12);
on obtient l'affichage suivant :
A la descente : 12 11 10 9 8 7 6 5 4 3 2 1
A la remonte : 1 2 3 4 5 6 7 8 9 10 11 12
Dans cet exemple, la communication descendante rendue possible par le passage d'une valeur
est utilise la fois pour afficher les diffrentes valeurs et pour garantir que la rcursion n'est
pas infinie. L'absence de communication ascendante est, pour sa part, mise en vidence par le
fait que la mise 0 du paramtre (9) ne perturbe aucunement l'affichage des valeurs lors de la
remonte rcursive.
J-L Pris - 10/02/06
C++ - Leon 15
Fonctions rcursives
7/9
void CExemple::affiche()
{
cout << compteur << " ";
CExemple instanceLocale(compteur-1);
if (compteur > 1)
instanceLocale.affiche();
else
cout << "\nA la remonte : ";
cout << compteur << " ";
}
L'excution du code suivant
1
2
3
CExemple uneInstance(12);
cout << "A la descente : ";
uneInstance.affiche();
produirait alors le mme affichage que celui obtenu avec la version prcdente :
A la descente : 12 11 10 9 8 7 6 5 4 3 2 1
A la remonte : 1 2 3 4 5 6 7 8 9 10 11 12
La variable membre n'tant pas modifie, l'affichage effectu lors de la remonte rcursive est le
mme que celui effectu lors de la descente. Chaque excution enchsse s'effectue sur une
instance dont le compteur a t initialis avec une valeur infrieure d'une unit celle du
compteur de l'instance utilise par l'excution appelante. Cette initialisation permet la fois
l'affichage de valeurs diffrentes par les diffrents niveaux d'excution et l'achvement de la
rcursion.
C++ - Leon 15
Fonctions rcursives
8/9
La fonction compte() commence par vrifier (3) si la chane est vide. Si c'est le cas, le nombre
d'occurrences du caractre c y est ncessairement nul, et la fonction renvoie donc zro (4).
Dans le cas contraire, la fonction calcule le "nombre d'occurrences du caractre c" dans le
premier caractre de la chane. La rponse recherche est alors simplement la somme de ce
nombre et du nombre d'occurrences du caractre c dans le reste de la chane. La variable
locale onEnTientUn permet donc chacune des excutions enchsses de stocker le rsultat
du traitement d'un caractre, la somme de ces rsultats tant effectue lors de la remonte
rcursive. Remarquez qu'aucune variable ne contient jamais cette somme, car il ne s'agit que
du rsultat de l'valuation d'une expression, rsultat que le mcanisme de renvoi propage
jusqu' la fonction appelante. A titre d'illustration, l'appel
cout << compte("Il tait une fois un petit chaperon rouge", 't');
produirait videmment l'affichage du nombre 4.
La meilleure rponse est mon avis la suivante : si vous voyez comment crire une boucle qui
traite le problme, inutile de vous forcer imaginer une solution rcursive.
Cette rgle possde un corollaire selon lequel, si vous voyez comment crire une fonction
rcursive qui traite le problme, il n'est peut tre pas indispensable de vous forcer imaginer
une solution purement itrative.
Avant d'affirmer que ce corollaire ne vous concernera jamais, voyez le TD 15 et la Leon 20...
C++ - Leon 15
Fonctions rcursives
9/9
5) Lorsque la fonction rcursive est membre d'une classe, la rflexion suggre au point
prcdent doit prendre en compte le paramtre cach this.
6) Le reste du secret d'une fonction rcursive rside vraisemblablement dans la valeur qu'elle
renvoie.
7) Contrairement ce que les remarques prcdentes pourraient laisser croire, il existe des cas
o utiliser un appel rcursif est la solution la plus simple.