Post on 04-Apr-2015
Structures de donnéesIFT-2000
Abder AlikacemAbder Alikacem
La récursivité
Département d’informatique et de génie logiciel
Édition Septembre 2009
Plan du cours
I. Notions de baseII. Exemple simple (imp. de nombres)III. Limitations (suite de Fibonacci)IV. Arbres (introduction)V. Autres exemplesVI. Sous-séquence de somme maximaleVII. Backtracking
Notions de base
Définition
Un objet est récursif s'il est défini à partir de lui-même. Une fonction est récursive si elle peut s'appeler elle-même de façon directe ou indirecte
Quand ? Suite récurrentes (exemple : factorielle) Type récursifs
Liste non vide = premier + listeArbre binaire = racine + sous-arbre droit + sous-arbre
gauche Définitions récursives
Grammaire : <E> ::= + (<E>,<E>)<E> ::= * (<E>,<E>)<E> := Nombre || identificateur
Récursivité
Idée générale :La réalisation d'une tâche par un algorithme récursifrepose sur deux éléments:
• La résolution partielle du problème d'une façon simple. La réalisation du reste de la tâche étant déléguée aux appels récursifs successifs.
• La détermination d'une condition d'arrêt qui permet d'arrêter la cascade d'appels récursifs. Résoudre le problème au moment où la condition d'arrêt est détectée correspond en général à résoudre un cas trivial de celui-ci.
Récursivité
Quatre règles régissent la récursivité:
1. Présence de cas de base pouvant être résolu sans récursivité2. Toujours progresser vers le cas de base à chaque appel récursif3. « Ayez la Foi »: avoir confiance que les appels récursifs
progressent réellement vers le cas de base.4. Intérêt composé: Ne jamais dédoubler du travail dans deux
appels récursifs différents.
Fonction récursive factorielle
Soit la fonction Factorielle, effectuant l’opération n!
int fact (int n){if (n = = 0) return 1;else return (n * fact (n-1));
}
Règle récursive : fact(n) = n * fact (n-1)Condition d’arrêt : fact (0) =1
PILE DES CONTEXTES
Idée générale
La notion de récursivité très liée au concept des piles.
Les piles sont des structures qui peuvent contenir plusieurs éléments. Toutefois, seul le dernier élément ajouté à la pile est accessible.
Une analogie peut être faite avec les piles d'assiettes. Seule l'assiette du dessus est immédiatement accessible. De la même façon, lorsqu'une assiette est ajoutée, celle-ci est nécessairement déposée sur le dessus de la pile.
PILE DES CONTEXTES
• A chaque appel d’une fonction récursive, les arguments transmis par valeur et les variables locales sont empilés.
• Ces valeurs constituent le contexte d’activation de l’appel.• A chaque retour le contexte situé en haut de la pile est
disponible• Les contextes sont indépendants
Assiette disponible
Ainsi, lorsqu'une fonction est appelée, ses paramètres formels, l'adresse de retour et les variables locales (automatiques) de la fonction appelée sont déposés sur la pile. Toutes ces données sont retirées de la pile à la finde l'exécution de la fonction. Conséquemment, lors d'appels récursifs, les variables de la fonction sont empilées en cascade.
PILE DES CONTEXTES
EXECUTION DE FACTORIELLE(3)
Retour au point appelant
Pile vide
(dépiler)
Return (3 *2)N=3(dépiler) => 1
Return (2 *1)N=2(dépiler) => 2
Return (1*1)N=1(dépiler) => 3
Return(1)N=04 (empiler)
N*fact(0)N=13 (empiler)
N*fact(1)N=22 (empiler)
N*fact(2)N=31 (empiler)
N° appel N° retour Contexte Action
Avantages et inconvénients
Fiabilité Solution naturelle et facile à concevoir :• si la fonction est récursive • quand la structure de données traitée est récursive
Avantages• Formulation compacte, claire et élégante.• Maîtrise des problèmes dont la nature même est récursive.
Désavantages• Possibilité de grande occupation de la mémoire.• Temps d'exécution peut être plus long.• Estimation difficile de la profondeur maximale de la récursivité.
Limitations: Fonction récursive Fibonnacci
long fibo (long valeur){ if (valeur <= 1) return valeur; else
return(fibo(valeur-1) + fibo(valeur-2));}
Soit la fonction Fibo (Fibonacci), effectuant l’opération f(n) = f(n-1) + f(n-2)
Exécution de Fibonnacci
Soit les appels effecutés pour fibo(4) :
fibo(4)
fibo(3)
fibo(2)
fibo(1) fibo(0)
fibo(1)
fibo(2)
fibo(0)fibo(1)
Danger de Fibonnacci
Note :• Cette fonction récursive effectue plusieurs appels au même calcul.
Par exemple pour déterminer f(n), on calcule d’abord f(n-1), puis au retour de l’appel, f(n-2) est calculé. Or, dans le calcul de f(n-1), f(n-2) est déjà calculé!
• Ce problème s’aggrave en descendant l’arborescence, puisque f(n-3) sera appelé à 3 reprises : chaque appel récursif entraînera de plus en plus d’appel redondant.
• Règle 4: Ne jamais dupliquer le travail par la résolution d’une même instance d’un problème dans plusieurs appels récursifs.
Champs d'application des algorithmes récursifs
• Structures de données définies récursivement (listes, arbres, graphes, etc.)
• Équations de récurrence (algébriques, vectorielles, booléennes, formelles, ensemblistes, etc.);
• Rétroaction ("backtracking").• …
Nous allons voir ensemble quelques exemples typiques sur la récursivité.
Les tours de Hanoï
Problème : • Soit n tours de tailles décroissantes sur un socle A, transférer
les n tours sur le socle B en utilisant un socle intermédiaire C.
Socle A Socle B Socle C
Déplacement d’une tour : on ne peut empiler qu’une tour de plus petite taille sur une autre tour
Paramétrage
void Hanoi(int n, socle& A, socle& B, socle&C);
n : nombre de toursA : socle de départ (valeur initiale : suite de n tours
décroissantes)B : socle de d’arrivée (valeur initiale : vide)C : socle intermédiaire (valeur initiale : vide)
Les tours de Hanoï : résolution
Socle A Socle B Socle C
Socle A Socle B Socle C
RESOLUTION RECURSIVE
Cas trivial (arrêt)n=1 : il suffit de déplacer l’unique tour de A vers B
Règle récursiven>1 : Hanoi (n-1, A ,C, B)
Déplacer (A,B) Hanoi (n-1, C, B, A)
FONCTION HANOI
void Hanoi (int n, socle& A, socle& B, socle&C){if (n = = 1) Déplacer (A,B);else {
Hanoi (n-1,A,C,B);Déplacer (A,B);Hanoi (n-1,C,B,A);
}}
A : départ B : arrivée C: intermédiaire
ARBRE DES APPELS (n=3)
Déplacer (A,B)
Déplacer (A,C)
Hanoi (3,A,B,C)
Hanoi (2,A,C,B)
Hanoi (1,B,C,A)Hanoi (1,A,B,C)
Déplacer (A,B) Déplacer (B,C)
Hanoi (2,C,B,A)
Déplacer (C,A)
Déplacer (C,B)
Déplacer (A,B)
Autres exemples
• Exemples démontrant des méthodes qui peuvent être implémentés non récursivement et des avantages de la récursivité.
• Ex : recherche dichotomique (binaire)• Ex : Dessin d’une règle
Recherche binaire (dichotomique)
Idée générale :• La recherche d’une valeur particulière dans un tableau ordonné
se fait par divisions successives du tableau en deux parties. Le fait que le tableau soit ordonné permet de déterminer rapidement la moitié dans laquelle se trouve l’élément recherché.
i = début –1j = fin +1 Tant que i et j ne se rencontrent pas Trouver le milieu du sous-tableau courant Si la valeur cherchée < tableau [milieu] j = milieu Si la valeur cherchée = tableau [milieu] Retourne milieu Si la valeur cherchée > tableau [milieu] i = milieuRetourne -1
Recherche dichotomique : Exemple d’exécution
Valeur Cherchée = 17; Début = 0, Fin = 10Valeur Cherchée = 17; Début = 0, Fin = 10
00 11 22 33 44 55 66 77 88 99 1010
-2-2 55 77 99 1111 1515 1717 2020 2121 3030 3939
iijj
milieumilieu
-1-1 1111
La valeur cherchée est dans la partie gauche du tableau
Valeur cherchée = tableau[milieu]Valeur cherchée > tableau[milieu]
La valeur cherchée est dans la partie droite du tableau
Valeur cherchée < tableau[milieu]
On retourne milieu
Recherche binaire programmation non-récursive
int RechercheBinaire( int * tab, int debut, int fin,int val){
int i = debut - 1; int j = fin + 1; while ( i+1 != j) {
int milieu = (i+j)/2; if ( val < tab[milieu]) j = milieu; if (val == tab[milieu]) return milieu; if (val> tab[milieu]) i = milieu;
} return -1;}
Fonction récursive pour la recherche binaire
int rechercheBinaire(int * tab, int debut, int fin, int val){
int milieu;if (debut > fin) return -1; else {
milieu= (debut + fin)/2; if( val == tab[milieu]) return milieu; else {
if( val < tab[milieu]) return(rechercheBinaire(tab,val,debut, milieu-1)); else {
return(rechercheBinaire(tab,val,milieu+1,fin)); }}
}}
Sous-séquence de somme maximale
Définition:• La technique de diviser et conquérir (divide-and-conquer) est une
méthode de résolution de problèmes basée sur la récursivité:
• Diviser en plusieurs sous problèmes résolus récursivement.
• Conquérir, où la solution du problème original est composé de la solution des sous-problèmes.
Sous-séquence de somme maximale
Définition:
Étant donnée des entiers (possiblement négatifs) A1, A2,. . . , AN, trouver et
identifier la séquence correspondante à la valeur maximale de .
La somme de la sous-séquence est nulle si tous les entiers sont négatifs.
j
ik kA
Sous-séquence de somme maximale
Exemple:
Soit les entiers suivants: {4, -3, 5, -2, -1, 2, 6, -2}. On divise cet ensemble en deux:
Première moitié Deuxième moitié
Valeurs
Sommes
4 -3 5 -2 -1 2 6 -2
4* 0 3 -2 -1 1 7* 5
Sommes à partir du centre (* indique la somme maximale pour chaque moitié)
Sous-séquence de somme maximale
Cas possible:Cas possible:
• Cas 1: La séquence est situé dans la première moitié.• Cas 2: La séquence est situé dans la deuxième moitié.• Cas 3: La séquence commence dans la première moitié et se
termine dans la deuxième moitié.
Algorithme de la sous-séquence de somme maximale
Sommaire de l’algorithme:
1. Déterminer récursivement la sous-séquence de somme maximale uniquement dans la première moitié.
2. Déterminer récursivement la sous-séquence de somme maximale uniquement dans la deuxième moitié.
3. Avec deux boucles consécutives, déterminer la sous-séquence de somme maximale qui débute dans la première moitié et qui termine dans la deuxième moitié.
4. Choisir la somme la plus élevée des trois.
Fonction récursive pour la sous-séquence de somme maximale
template<class Comparable>Comparable maxSubSum( const vector<Comparable> & a, int left, int
right){ Comparable maxLeftBorderSum = 0, maxRightBorderSum = 0; Comparable leftBorderSum = 0, rightBorderSum = 0; int center = ( left + right ) / 2;
if(left == right) return a[left] > 0 ? a[left] : 0; Comparable maxLeftSum = maxSubSum(a,left,center); Comparable maxRightSum = maxSubSum(a,center+1,right); for(int i=center; i>=left ;i--) { leftBorderSum += a[i]; if( leftBorderSum > maxLeftBorderSum ) maxLeftBorderSum = leftBorderSum; } . . .
for(int j=center+1; j<=right; j++) { rightBorderSum += a[j]; if( rightBorderSum > maxRightBorderSum ) maxRightBorderSum = rightBorderSum; }
return max3( maxLeftSum, maxRightSum, maxLeftBorderSum + maxRightBorderSum );}
template <class Comparable>Comparable maxSubsequenceSum( const vector<Comparable> & a){ return a.size() > 0 ? maxSubSum( a, 0, a.size() – 1) : 0;}
Fonction récursive pour la sous-séquence de somme maximale
Sous-séquence de somme maximale (Exemple)
3 2 -5 -1 0 -1 2
3 2 -5 -1 0 -1 2
3 2 -5 -1 0 -1 2
2 -5 -1 0 -1 2
2
3
2 0 0 0 0
0 12
00 0
0
max(2,0,2+0)
2
max(3,2,3+2)
3
0 0
max(0,0,0+0)
0 2
max(0,2,0+2)
max(0,2,0+1)
Sous-séquence de somme maximale (Exemple)
3 2 -5 -1 0 -1 2
3 2 -5 -1 0 -1 2
3 2 -5 -1 0 -1 2
2 -5 -1 0 -1 2
2
3
2 0 0 0 0
0 12
00 0
0
max(2,0,2+0)
2
max(3,2,3+2)
3
0 0
max(0,0,0+0)
0 2
max(0,2,0+2)
max(0,2,0+1)
max(5,2,0+0)=5
Algorithme à essais successifs
Problématique Pour résoudre certains types de problèmes, il peut être souvent utile de procéder à l'exploration systématique des différentes solutions possibles. Il est donc essentiel de développer des techniques algorithmiques qui permettent une telle exploration.
AES ou encore algorithme de back trackingidée : construire progressivement une solution à un problème donné en essayant toutes les possibilités à chaque étape de la construction.
Remarque : la complexité de ce genre d ’algorithme est de nature exponentielle => à utiliser si l ’on n ’en connaît pas de meilleur.
Algorithme à essais successifs
AES ou encore algorithme de back tracking
On part d’une situation initiale So (ex : échiquier vide) pour aller vers une situation finale Sf (ex : Huit reines placées). Plusieurs situations peuvent répondre au problème. La situation finale est atteinte par le passage successif de situations jusqu’à atteindre Sf.
Le passage de la situation Si à Si+1 se fait par une action.
Pour chaque situation Si on a le choix entre plusieurs actions menant à des situations éventuellement différentes.
Algorithme à essais successifs
MODELISATION Une fonction Succès(S) qui permet de savoir si une situation S répond au problème. Une fonction EnsembleChoix(S) qui permet d ’obtenir à partir d ’une situation S donnée l’ensemble des actions possibles pour passer à une situation suivante.Une fonction Successeur(S,A) qui donne la situation obtenue par l’application de l’action A à la situation S.Pour chaque situation Si on a le choix entre plusieurs actions menant à des situations éventuellement différentes.On appelle chemin une suite d ’actions : <A0, A1, …, An>
Fonction AES (s:Situation) : (Chemin,Situation)Variables trouvé, choixsi succès(s) alors
trouvé=vrai; retourner(vide,s)sinon
trouvé=faux; choix <- ensembleChoix(s)tant que choix n’est pas vide et non trouvé faire
action <-un élément de choixchoix <-choix - {action}(chemin,situation)<- (AES (successeur(s,action)))
fin tant quesi trouvé alors
ajouter(action, chemin),sf))sinon retourner (Erreur Chemin, Erreur action);fin si
fin si
Algorithme
Backtracking
Exemple. Calcul du trajet du robot
Il s'agit de concevoir un algorithme qui permet à un robot de trouver un chemin menant à la sortie d'un labyrinthe. Pour ce faire le robot explore systématiquement les quatre directions vers lesquelles il peut se déplacer. De plus, afin d'éviter que le robot ne se retrouve sur des cases déjà explorées chacune de celles-ci seront marquées à mesure qu'elles sont visitées. C'est de façon récursive que les cases faisant partie du chemin seront identifiées.
Algorithme du retour-arrière
Plan du site
N
EO
S
Légende
: point de départ
: point d’arrivée
: mur ou obstache
E
S
0
A B C D E F G H I
S
E1
2
3
4
5
6
Programmation du trajet d’un robot mobile
#define Y 9 /* Les dimensions du labyrinthe */#define X 7char laby[X][Y] = { {'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X'}, {'X', ' ', ' ', 'X', ' ', ' ', ' ', ' ', 'X'}, {'X', ' ', 'X', 'X', ' ', 'X', 'X', ' ', 'X'}, {'X', ' ', ' ', ' ', ' ', ' ', 'X', 'X', 'X'}, {'X', 'X', 'X', ' ', 'X', ' ', 'X', ' ', 'X'}, {'X', 's', ' ', ' ', ' ', ' ', ' ', ' ', 'X'}, {'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X', 'X'}};int main(){ int i, j; bool Parcours(int x, int y); if (Parcours(1,1)) { // Imprimer le résultat de la recherche for (i = 0; i < X; i++, cout<<endl) for (j = 0; j < Y; j++) cout<<laby[i][j]; } else cout<<"Il n'existe aucun chemin.\n"; return 0;}
Programmation du trajet d’un robot mobile (suite)
bool Parcours(int x, int y){ if (laby[x][y] == 's') { /* on a trouvé la sortie */ laby[x][y] = 'S'; return true; } else if (laby[x][y] == ' ') { /* la case est libre */ laby[x][y] = 'o'; /* marquer la case visitée */ if (Parcours(x-1, y)) /* Parcours nord */ return(true); else if (Parcours(x, y+1)) /* Parcours est */ return(true); else if (Parcours(x+1, y)) /* Parcours sud */ return(true); else if (Parcours(x, y-1)) /* Parcours ouest */ return(true); else laby[x][y] = '-'; /* on laisse une marque */ } return false;}
Retour en arrière - exercice
• Quel est le chemin parcouru? Combien de retours en arrière?
• Utiliser des appels récursifs et l’ordre d’appel suivant:• Nord• Sud• Est• Ouest
0 1 2 3 4 5 6 7 8 9
A
B S
C
D
E
F EG
H
I
J
Retour en arrière - réponse
0 1 2 3 4 5 6 7 8 9
A
B
C
D
E
F
G
H
I
J
X
E
S
oo
o
o
oo X
X
o
o
o
o
o
o o o
o
oX
o
o o
o
o o o
o
o
o
o o
oX
Xo
o
L’ELIMINATION DE LA RECURSIVITE
Pourquoi ?Problème d’encombrement mémoire= > Diminuer la taille de la pile
Comment ? Écrire un algorithme itératif équivalent Gérer dans l’algorithme la pile des sauvegardes => ne garder
que celles utiles
Transformer un algorithme récursif en une version itérative équivalente qui conduise à une diminution de la pile.
Transformation : un cas simple
void fonction (H x){ if (c(x)) A0(x);else { A1(x) ;
fonction(F1(x)); }
void fonction (H x){ while (! c(x)) {A1(x);
x=F1(x);}
A0(x);}
Un seul appel récursif terminal.=> La pile est inutile.=> Version itérative équivalente qui conduise à l ’élimination de la pile.
Transformation : un cas simple
void enumerer (int n){ if (n <= 0) cout << "fin";else { cout << n ;
enumerer (n - 1); }
void enumerer (int n){ while (n > 0) {
cout << n ; n = n - 1;
} cout << "fin";
}
c(x) : n <= 0
A0 : cout << "fin";
A1 : cout << n;
F1 (x) : n = n - 1;
Transformation : autre modèle
T fonction (T2 x){ if (c(x)) return f(x);
else { return u (g(x), fonction(h(x))); }
T fonction (T2 x){ if (c(x)) return f(x);
else {p=g(x); x=h(x); while (! c(x)) {
p = u(p,g(x));
x=h(x);}return u (p,f(x)); }
}
Exemple : un seul appel récursif
Version récursive : ! c(x) => u [ g(x),fonction(h(x)) ] puis c(h(x)) => u [ g(x),f(h(x)) ]
Version itérative : ! c(x) => p=g(x); x=h(x); puis c(h(x)) => u [ g(x),f(h(x)) ]
même résultat
Exemple : deux appels
Version récursive! c(x) => u [ g(x),fonction (h(x)) ]! c(h(x)) => u [ g(x), u [ g(h(x)),fonction (h2(x)) ] ] c(h2 (x)) => u [ g(x), u [ g(h(x)),f(h2(x)) ] ]
Version itérative ! c(x) => p=g(x); x=h(x); ! c(h(x)) => p = u [ g(x), g(h(x))]
x= h2 (x) c(h2 (x)) => u [u [ g(x), g(h(x))] ,f(h2(x)) ]
même résultat
u doit être une relation associative
Factorielle
int fact (int x){ if (x = = 0) return 1;else return (x * fact (x-1)); }
c(x) : x = =0
f(x) =1
u : *
g(x) : x
h(x)=x-1
int fact (int x){if (x==0) return 1;else {p=x; x=x-1;
while (!(x==0)){p = p * x ;x= x-1;
}return p*1;
}}
Transformation : autre modèle
algorithme P (T x){si (c(x)) alors A0(x)sinon { A1(x)
P(F1(x)) A2(x) P(F2(x)). . .Am(x) P(Fm(x))
Am+1(x) }
}
ARBRE DES APPELS (m=2)
P(x)
P [F1(x)]
P [F1(F1(x))]
A0On remonte
A1
A1
P [F2(F1(x))]
A2
A0On remonte
A3On remonte
A2
P [F2(x)]A3
Transformation : autre modèle
algorithme P (T x){empiler (x,1) // marque fin de pile
REPETER { TANT QUE (non c(x)) FAIRE
A1(x)empiler (x,2) x <- F1(x)
FAIT A0(x)depiler (x, numret)TANT QUE(numret = = m+1)FAIRE
Am+1(x) depiler (x, numret)
FAIT
SI (numret != 1) alors Anumret(x)empiler
(x,numret+1) x <- Fnumret(x)
FSI JUSQU ’A (numret = = 1)
depiler (x, numret) // fin de // pile
On empile :• le contexte (données)• n° de l ’action suivante
FONCTION HANOI
void Hanoi (int n, socle& A, socle& B, socle&C) {if (n = = 1)Déplacer (A,B); // A0(x) else { // A1(x) : action vide
Hanoi (n-1,A,C,B); // P(F1(x)) Déplacer (A,B); // A2(x) Hanoi (n-1,C,B,A);} // P(F2(x))
}
c(x) : n = =1A0(x) : deplacer (A,B)A1(x) : action videF1(x) : permuter(B,C); n=n-1;A2(x) : deplacer (A,B)F2(x) : permuter(A,C); n=n-1;A3(x) : action vide
m=2
Am+1 : action vide
Dernier appel terminal =>Economie mémoire (pile)
Transformation : Hanoi
void Hanoi (int n, ...) {empiler (x,1) REPETER {
TANT QUE (non c(x)) FAIRE //action videempiler (n,a,b,c,2) permuter(B,C); n=n-1;
FAIT deplacer (A,B); //A0(x)depiler (n,a,b,c,numret)TANT QUE (numret = m+1) FAIRE
Am+1(x) depiler (x, numret)
FAIT
SI (numret != 1) alors deplacer (A,B); //A2(x)empiler (x,numret+1)
permuter(A,C); n=n-1;FSI
JUSQU ’A (numret = = 1)depiler (n,a,b,c,numret)
Dernier appel est terminal
Pas d ’appel récursif suivant