Vous êtes sur la page 1sur 11

Chapitre 7

Rcursivit
7.1 La notion de rcursivit
7.1.1 La dcomposition en sous-problmes
Le processus danalyse permet de dcomposer un problme en sous-problmes plus simples.
A leur tour, ces sous-problmes seront dcomposs jusqu un niveau doprations lmentaires,
faciles raliser.
Exemple : afcher un rapport avec les lves dune classe, groups par la note obtenue un examen,
en ordre dcroissant des notes et en ordre alphabtique pour chaque note.
Une dcomposition possible de ce problme en sous-problmes :
introduction des donnes (noms des lves et notes)
tri des lves en ordre dcroissant des notes
pour chaque note obtenue, en ordre dcroissant, rpter
extraction des noms dlves qui ont obtenu cette note
calcul du nombre dlves qui ont obtenu cette note
tri de ces lves en ordre alphabtique
afchage de la note, du nombre dlves qui ont cette note et des noms de ces lves
Chacun de ces sous-problmes pourra tre dcompos son tour en sous-sous-problmes, etc. En
programmation, la dcomposition en sous-problmes correspond au dcoupage dun programme en
sous-programmes ; a chaque sous-problme correspond un sous-programme.
La dcomposition ci-dessus dcrit lalgorithme de rsolution du problme en utilisant lappel aux
sous-programmes qui traitent les sous-problmes.
7.1.2 Dcomposition rcursive
Dans certains cas, le sous-problme est une illustration du problme initial, mais pour un cas plus
simple. Par consquent, la solution du problme sexprime par rapport elle-mme ! On appelle ce
phnomne rcursivit.
Exemple : calcul de la factorielle dune valeur entire positive n (n! = 1 * 2 * .... * n)
Mais, n! = (1 * 2 * .... * (n-1) ) * n, donc n ! = (n-1) ! * n.
Pour calculer la valeur de n!, il suft donc de savoir calculer (n-1) ! et ensuite de multiplier cette
valeur par n. Le sous-problme du calcul de (n-1) ! est le mme que le problme initial, mais pour un
cas plus simple, car n 1 < n.
1
7.1. LA NOTION DE RCURSIVIT CHAPITRE 7. RCURSIVIT
Conclusion : le problme a t rduit lui-mme, mais pour un cas plus simple.
En programmation, le sous-programme qui traite le problme fait un appel lui-mme ( !) pour
traiter le cas plus simple, ce qui revient un appel avec des paramtres diffrents (plus simples). On
appelle cela un appel rcursif.
La fonction factorielle aura alors la forme suivante :
int factorielle (int n){
int sous_resultat = factorielle (n-1); //appel recursif
int resultat = sous_resultat *n;
return resultat;
}
Remarque trs importante : un appel rcursif peut produire lui-mme un autre appel rcursif, etc,
ce qui peut mener une suite innie dappels. En loccurrence, la fonction ci-dessus est incor-
recte. Il faut arrter la suite dappels au moment o le sous-problme peut tre rsolu directe-
ment. Dans la fonction prcdente, il faut sarrter (ne pas faire dappel rcursif) si n = 1, car
dans ce cas on connat le rsultat (1 ! = 1).
Conclusion : dans tout sous-programme rcursif il faut une condition darrt.
La version correcte de la fonction factorielle est la suivante :
int factorielle (int n){
int resultat;
if(n==1) resultat = 1;
else {
int sous_resultat = factorielle (n-1); //appel recursif
resultat = sous_resultat *n;
}
return resultat;
}
Remarque : la fonction factorielle peut tre crite dune manire plus compacte, mais nous avons
prfr cette version pour mieux illustrer les sections suivantes. Normalement, on na pas besoin
des variables locales et on peut crire directement :
int factorielle (int n){
if(n==1) return 1;
else return factorielle(n-1) *n;
}
Remarque : lexistence dune condition darrt ne signie pas que lappel rcursif sarrte grce
celle-ci. Prenons lexemple de lappel factorielle(-1) : cel produit un appel factorielle(-2),
qui produit un appel factorielle(-3), etc. La condition darrt (n=1) nest jamais at-
teinte et on obtient une suite innie dappels.
Conclusion : dans un sous-programme rcursif, il faut sassurer que la condition darrt est atteinte
aprs un nombre ni dappels.
Remarque : la condition darrt doit tre choisie avec soin. Elle doit correspondre en principe au
cas le plus simple quon veut traiter, sinon certains cas ne seront pas couverts par le sous-
programme. Dans notre cas, la fonction factorielle ne sait pas traiter le cas n=0, qui est
2 NFA031 c CNAM 2012
CHAPITRE 7. RCURSIVIT 7.1. LA NOTION DE RCURSIVIT
pourtant bien dni (0 ! = 1) et qui respecte la relation de dcomposition rcursive (1 ! = 0 ! *
1).
Le programme complet qui utilise la fonction factorielle, modie pour tenir compte des
remarques ci-dessus, est le suivant :
public class TestFactorielle{
static int factorielle (int n){
int resultat;
if(n<0) throw new MauvaisParametre();
else if(n==0) resultat = 1;
else {
int sous_resultat = factorielle (n-1); //appel recursif
resultat = sous_resultat *n;
}
return resultat;
}
public static void main(String[] args){
Terminal.ecrireString("Entrez un entier positif : ");
int x = Terminal.lireInt();
Terminal.ecrireStringln(x + "! = " + factorielle(x));
}
}
class MauvaisParametre extends Error{}
7.1.3 Rcursivit directe et indirecte
Quand un sous-programme fait appel lui mme, comme dans le cas de la factorielle, on ap-
pelle cela rcursivit directe. Parfois, il est possible quun sous-programme A fasse appel un sous-
programme B, qui lui mme fait appel au sous-programme A. Donc lappel qui part de A atteint de
nouveau A aprs tre pass par B. On appelle cela rcursivit indirecte (en fait, la suite dappels qui
part de A peut passer par plusieurs autres sous-programmes avant darriver de nouveau A!).
Exemple : un exemple simple de rcursivit indirecte est la dnition rcursive des nombres pairs et
impairs. Un nombre n positif est pair si n-1 est impair ; un nombre n positif est impair si n-1
est pair. Les conditions darrt sont donnes par les valeurs n=0, qui est paire et n=1, qui est
impaire.
Voici le programme complet qui traite ce problme :
public class TestPairImpair{
static boolean pair(int n){
if(n<0) throw new MauvaisParametre();
else if(n==0) return true;
else if(n==1) return false;
else return impair(n-1);
}
static boolean impair(int n){
if(n<0) throw new MauvaisParametre();
else if(n==0) return false;
NFA031 c CNAM 2012 3
7.2. EVALUATION DUN APPEL RCURSIF CHAPITRE 7. RCURSIVIT
else if(n==1) return true;
else return pair(n-1);
}
public static void main(String[] args){
Terminal.ecrireString("Entrez un entier positif : ");
int x = Terminal.lireInt();
if(pair(x)) Terminal.ecrireStringln("nombre pair");
else Terminal.ecrireStringln("nombre impair");
}
}
class MauvaisParametre extends Error{}
La rcursivit indirecte est produite par les fonctions pair et impair : pair appelle impair
et impair appelle son tour pair.
Un autre exemple, qui combine les deux types de rcursivit, est prsent ci-dessous :
Exemple : calculer les suites de valeurs donnes par les relations suivantes :
x
0
= 1 ; x
n
= 2*y
n1
+ x
n1
y
0
= -1 ; y
n
= 2*x
n1
+ y
n1
Les fonctions rcursives qui calculent les valeurs des suites x et y sont prsentes ci-dessous. On
retrouve dans chaque fonction la fois de la rcursivit directe (x fait appel x et y y) et de la
rcursivit indirecte (x fait appel y, qui fait appel x, etc). Lexemple ne traite pas les situations de
mauvaises valeurs de paramtres (d n < 0).
int x (int n){
if(n==0) return 1;
else return 2*y(n-1) + x(n-1);
}
int y (int n){
if(n==0) return -1;
else return 2*x(n-1) + y(n-1);
}
7.2 Evaluation dun appel rcursif
Comment est-il possible dappeler un sous-programme pendant que celui-ci est en train de sex-
cuter ? Comment se fait-il que les donnes gres par ces diffrents appels du mme sous-programme
ne se mlangent pas ? Pour rpondre ces questions il faut dabord comprendre le modle de m-
moire dans lexcution des sous-programmes en Java.
7.2.1 Modle de mmoire
Chaque sous-programme Java (le programme principal main aussi) utilise une zone de mmoire
pour stocker ses paramtres et ses variables locales. De plus, une fonction rserve aussi dans sa zone
de mmoire une place pour le rsultat retourn. Prenons pour exemple la fonction suivante :
4 NFA031 c CNAM 2012
CHAPITRE 7. RCURSIVIT 7.2. EVALUATION DUN APPEL RCURSIF
boolean exemple (int x, double y){
int [] t = new int[3];
char c;
...
}
La zone de mmoire occupe par cette fonction est illustre dans la gure 7.1
y
t
c
x
return
exemple
FIGURE 7.1 Zone de mmoire occupe par la fonction exemple
Lallocation de cette zone de mmoire se fait au moment de lappel du sous-programme, dans une
zone de mmoire spciale du programme, appel la pile. Les zones de mmoire des sous-programmes
sont empiles suivant lordre des appels et dpiles ds que le sous-programme se termine. Par cons-
quent, la zone de mmoire dun sous-programme nexiste physiquement que pendant que le sous-
programme est en cours dexcution.
Supposons que le programme principal qui appelle la fonction exemple a la forme suivante :
public class Principal{
static boolean exemple (int x, double y){ ... }
public static void main(String[] args){
double x;
int n;
boolean c;
.........
c = exemple(n, x);
.........
}
}
Le contenu de la pile pendant lappel de la fonction exemple est prsent dans la gure 7.2.
x
y
c
main
y
t
c
x
return
exemple
empilement
dpilement
FIGURE 7.2 La pile pendant lappel de fonction exemple
NFA031 c CNAM 2012 5
7.3. COMMENT CONCEVOIR UN SOUS-PROGRAMME RCURSIF? CHAPITRE 7. RCURSIVIT
Avant lappel, tout comme aprs la n de lexcution de la fonction exemple, la pile ne contient
que la zone de main. La place libre par exemple sera occupe par un ventuel appel ultrieur
dun autre sous-programme (peut-tre mme exemple, sil est appel plusieurs fois par main!).
Remarque : Il y a une sparation nette entre les variables locales des diffrents sous-programmes,
car elles occupent des zones de mmoire distinctes (ex. les variables x, y et c).
Conclusion : la pile contient un moment donn les zones de mmoire de lenchanement de sous-
programmes en cours dexcution, qui part du programme principal.
7.2.2 Droulement des appels rcursifs
Dans le cas des programmes rcursifs, la pile est remplie par lappel dun mme sous-programme
(qui sappelle soi-mme). Prenons le cas du calcul de factorielle(2) : le programme principal
appelle factorielle(2), qui appelle factorielle(1), qui appelle factorielle(0), qui
peut calculer sa valeur de retour sans autre appel rcursif. La valeur retourne par factorielle(0)
permet factorielle(1) de calculer son rsultat, ce qui permet factorielle(2) den
calculer le sien et de le retourner au programme principal.
Lenchanement des appels et des calculs est illustr dans la gure 7.3.
resultat
sous_resultat
n
return
factorielle(2)
resultat
sous_resultat
n
return
factorielle(1)
resultat
sous_resultat
n
return
factorielle(0) main
x 2 2 1 0
1
1
1
1
1
1 1
1
2
2
2
FIGURE 7.3 Enchanement des appels rcursifs pour factorielle(2)
Chaque instance de factorielle rcupre le rsultat de lappel rcursif suivant dans la variable
sous_resultat, ce qui lui permet de calculer resultat et de le retourner son appelant.
7.3 Comment concevoir un sous-programme rcursif ?
Dans lcriture des programmes rcursifs on retrouve gnralement les tapes suivantes :
1. Trouver une dcomposition rcursive du problme
(a) Trouver llment de rcursivit qui permet de dnir les cas plus simples (ex. une valeur
numrique qui dcrot, une taille de donnes qui diminue).
(b) Exprimer la solution dans le cas gnral en fonction de la solution pour le cas plus simple.
2. Trouver la condition darrt de rcursivit et la solution dans ce cas
Vrier que la condition darrt est atteinte aprs un nombre ni dappels rcursifs dans tous
les cas
3. Runir les deux tapes prcdentes dans un seul programme
6 NFA031 c CNAM 2012
CHAPITRE 7. RCURSIVIT 7.3. COMMENT CONCEVOIR UN SOUS-PROGRAMME RCURSIF ?
c1
s1
s
FIGURE 7.4 Dcomposition rcursive dune chane s en un premier caractre c1 et le reste de la
chane s1
Exemple : calcul du nombre doccurrences n dun caractre donn c dans une chane de caractres
donne s.
1. Dcomposition rcursive
(a) Elment de rcursivit : la chane, dont la taille diminue. Le cas plus simple est la chane
sans son premier caractre (notons-la s1) - voir la gure 7.4.
(b) Soit n1 le nombre doccurrences de c dans s1 (la solution du problme plus simple).
Soit c1 le premier caractre de la chane initiale s.
Si c1 = c, alors le rsultat est n = n1 + 1, sinon n = n1.
2. Condition darrt : si s= "" (la chane vide) alors n = 0.
La condition darrt est toujours atteinte, car toute chane a une longueur positive et en dimi-
nuant de 1 la taille chaque appel rcursif on arrive forcment la taille 0.
3. La fonction sera donc la suivante :
int nbOccurrences (char c, String s){
if(s.length() == 0) return 0; //condition darret: chaine vide
else{
int sous_resultat = nbOccurrences(c, s.substring(1, s.length()-1));
if(s.charAt(0) == c) return 1 + sous_resultat;
else return sous_resultat;
}
}
7.3.1 Sous-programmes rcursifs qui ne retournent pas de rsultat
Les exemples prcdents de rcursivit sont des calculs rcursifs, reprsents par des fonctions
qui retournent le rsultat de ce calcul. La dcomposition rcursive montre comment le rsultat du
calcul dans le cas plus simple sert obtenir le rsultat nal.
La relation entre la solution du problme et la solution du cas plus simple est donc une relation
de calcul (entre valeurs).
Toutefois, dans certains cas le problme ne consiste pas en un calcul, mais en une action rcursive
sur les donnes (afchage, modication de la valeur). Dans ce cas, laction dans le cas plus sim-
ple reprsente une partie de laction raliser dans le cas gnral. Il sagit donc dune relation de
composition entre actions.
Exemple : afchage des lments dun tableau t en ordre inverse celui du tableau.
Cet exemple permet dillustrer aussi la mthode typique de dcomposer rcursivement un tableau
Java. La diffrence entre un tableau et une chane de caractres en Java est quon ne dispose pas de
NFA031 c CNAM 2012 7
7.4. RCURSIVIT ET ITRATION CHAPITRE 7. RCURSIVIT
fonctions dextraction de sous-tableaux (comme la mthode substring pour les chanes de carac-
tres). On peut programmer soi-mme une telle fonction, mais souvent on peut viter cela, comme
expliqu ci-dessous.
La mthode la plus simple est de considrer comme lment de rcursivit non pas le tableau,
mais lindice du dernier lment du tableau. Ainsi, le cas plus simple sera non pas un sous-tableau,
mais un indice de dernier lment plus petit - voir la gure 7.5.
t
n n1
FIGURE 7.5 Dcomposition rcursive dun tableau t en utlisant lindice n du dernier lment
1. Dcomposition rcursive
(a) Elment de rcursivit : lindice n du dernier lment du tableau t.
Le cas plus simple est lindice du dernier lment n-1, ce qui correspond au tableau t
sans son dernier lment.
(b) Laction pour le tableau t est dcompose de la manire suivante :
1. Afchage de t(n)
2. Afchage rcursif pour t sans le dernier lment.
2. Condition darrt : si n= 0 (un seul lment dans t) alors laction est dafcher cet lment
(t(0)).
La condition darrt est toujours atteinte, car tout tableau a au moins un lment, donc n >= 0.
En diminuant n de 1 chaque appel rcursif, on arrive forcment la valeur 0.
3. La fonction sera donc la suivante :
void afchageInverse(int[] t, int n){
if(n==0) Terminal.ecrireIntln(t(0)); //condition darret: un seul element
else{
Terminal.ecrireIntln(t(n));
afchageInverse(t, n-1);
}
}
7.4 Rcursivit et itration
Par lappel rpt dun mme sous-programme, la rcursivit permet de raliser des traitements
rptitifs. Jusquici, pour raliser des rptitions nous avons utilis les boucles (itration). Suivant le
type de problme, la solution sexprime plus naturellement par rcursivit ou par itration. Souvent il
est aussi simple dexprimer les deux types de solution.
Exemple : la solution itrative pour le calcul du nombre doccurrences dun caractre dans une chane
est comparable en termes de simplicit avec la solution rcursive.
int nbOccurrences (char c, String s){
int accu = 0;
for(int i=0; i<s.length(); i++)
8 NFA031 c CNAM 2012
CHAPITRE 7. RCURSIVIT 7.4. RCURSIVIT ET ITRATION
if(s.charAt(i)==c) accu++;
return accu;
}
En principe tout programme rcursif peut tre crit laide de boucles (par itration), sans rcur-
sivit. Inversement, chaque type de boucle (while, do...while, for) peut tre simul par rcursivit. Il
sagit en fait de deux manires de programmer diffrentes. Utiliser lune ou lautre est souvent une
question de got et de style de programmation - cependant chacune peut savrer mieux adapte que
lautre certaines classes de problmes.
7.4.1 Utilisation de litration
Un avantage important de litration est lefcacit. Un programme qui utilise des boucles dcrit
prcisment chaque action raliser - ce style de programmation est appel impratif ou procdural.
Le code compil dun tel programme est une image assez dle des actions dcrites par le programme,
traduites en code machine. Les choses sont diffrentes pour un programme rcursif.
Conclusion : si on recherche lefcacit (une excution rapide) et le programme peut tre crit sans
trop de difcults en style itratif, on prfrera litration.
7.4.2 Utilisation de la rcursivit
Lavantage de la rcursivit est quelle se situe un niveau dabstraction suprieur par rapport
litration. Une solution rcursive dcrit comment calculer la solution partir dun cas plus simple - ce
style de programmation est appel dclaratif. Au lieu de prciser chaque action raliser, on dcrit ce
quon veut obtenir - cest ensuite au systme de raliser les actions ncessaires pour obtenir le rsultat
demand.
Souvent la solution rcursive dun problme est plus intuitive que celle itrative et le programme
crire est plus court et plus lisible. Nanmoins, quelquun habitu aux boucles et au style impra-
tif peut avoir des difcults utiliser la rcursivit, car elle correspond un type de raisonnement
particulier.
Le style dclaratif produit souvent du code moins efcace, car entre le programme descriptif et le
code compil il y a un espace dinterprtation rempli par le systme, difcile optimiser dans tous
les cas. Cependant, les langages dclaratifs offrent un confort nettement suprieur au programmeur
(comparez SQL avec un langage de bases de donnes avec accs enregistrement par enregistrement).
Dans le cas prcis de la rcursivit, celle-ci est moins efcace que litration, car la rptition est
ralise par des appels successifs de fonctions, ce qui est plus lent que lincrmentation dun compteur
de boucle.
Dans les langages (comme Java) qui offrent la fois litration et la rcursivit, on va prfrer la
rcursivit surtout dans les situations o la solution itrative est difcile obtenir, par exemple :
si les structures de donnes manipules sont rcursives (ex. les arbres).
si le raisonnement lui mme est rcursif.
7.4.3 Exemple de raisonnement rcursif : les tours de Hanoi
Un exemple trs connu de raisonnement rcursif apparat dans le problme des tours de Hanoi. Il
sagit de n disques de tailles diffrentes, troues au centre, qui peuvent tre empils sur trois piliers.
Au dbut, tous les disques sont empils sur le pilier de gauche, en ordre croissant de la taille, comme
NFA031 c CNAM 2012 9
7.4. RCURSIVIT ET ITRATION CHAPITRE 7. RCURSIVIT
dans la gure suivante. Le but du jeu est de dplacer les disques un par un pour reconstituer la tour
initiale sur le pilier de droite. Il y a deux rgles :
1. on peut seulement dplacer un disque qui se trouve au sommet dune pile (non couvert par un
autre disque) ;
2. un disque ne doit jamais tre plac sur un autre plus petit.
Le jeu est illustr dans la gure 7.6.
FIGURE 7.6 Les tours de Hanoi
La solution sexprime trs facilement par rcursivit. On veut dplacer une tour de n disques du
pilier de gauche vers le pilier de droite, en utilisant le pilier du milieu comme position intermdiaire.
1. Il faut dabord dplacer vers le pilier du milieu la tour de n-1 disques du dessus du pilier de
gauche (en utilisant le pilier de droite comme intermdiaire).
2. Il reste sur le pilier de gauche seul le grand disque la base. On dplace ce disque sur le pilier
de droite.
3. Enn on dplace la tour de n-1 disques du pilier du milieu vers le pilier de droite, au-dessus du
grand disque dj plac (en utilisant le pilier de gauche comme intermdiaire).
La procdure rcursive deplaceTour ralise cet algorithme et utilise la procdure deplaceUnDisque
pour afcher le dplacement de chaque disque individuel.
Remarque : Il est difcile dcrire un algorithme itratif pour ce problme. Essayez comme exercice
dcrire le mme programme avec des boucles !
public class Hanoi{
static void deplaceUnDisque (String source, String dest){
Terminal.ecrireStringln(source + " - " + dest);
}
static void deplaceTour(int taille, String source, String dest, String interm){
if(taille==1) deplaceUnDisque(source, dest);
else{
deplaceTour(taille-1, source, interm, dest);
deplaceUnDisque(source, dest);
deplaceTour(taille-1, interm, dest, source);
}
}
public static void main(String[] args){
Terminal.ecrireString("Combien de disques ? ");
int n = Terminal.lireInt();
deplaceTour(n, "gauche", "droite", "milieu");
10 NFA031 c CNAM 2012
CHAPITRE 7. RCURSIVIT 7.4. RCURSIVIT ET ITRATION
}
}
Lexcution de ce programme produit le rsultat suivant :
% java Hanoi
Combien de disques ? 3
gauche - droite
gauche - milieu
droite - milieu
gauche - droite
milieu - gauche
milieu - droite
gauche - droite
NFA031 c CNAM 2012 11

Vous aimerez peut-être aussi