Structures de données IFT-2000 Abder Alikacem La récursivité Département d’informatique et de génie logiciel Édition Septembre 2009
Plan du cours I.Notions de base II.Exemple simple (imp. de nombres) III. III. Limitations (suite de Fibonacci) IV. IV.Arbres (introduction) V. V.Autres exemples VI. Sous-séquence de somme maximale VII. 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 + liste Arbre binaire = racine + sous-arbre droit + sous-arbre gauche Définitions récursives Grammaire : ::= + (, ) ::= * (, ) := Nombre || identificateur
Récursivité Idée générale : La réalisation d'une tâche par un algorithme récursif repose 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écursif 3.« 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 fin de 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° appelN° retourContexteAction
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 tours A : 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 BSocle C Socle A Socle B Socle C
RESOLUTION RECURSIVE CCas trivial (arrêt) n=1 : il suffit de déplacer l’unique tour de A vers B RRègle récursive n>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épartB : arrivéeC: 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 –1 j = 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 = milieu Retourne -1
Recherche dichotomique : Exemple d’exécution Valeur Cherchée = 17; Début = 0, Fin = i j milieu 11 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) A 1, A 2,..., A N, 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.
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 * * 5 Sommes à partir du centre (* indique la somme maximale pour chaque moitié)
Sous-séquence de somme maximale 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 Comparable maxSubSum( const vector & 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 Comparable maxSubsequenceSum( const vector & 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) max(2,0,2+0) 2 max(3,2,3+2) max(0,0,0+0) 02 max(0,2,0+2) max(0,2,0+1)
Sous-séquence de somme maximale (Exemple) max(2,0,2+0) 2 max(3,2,3+2) max(0,0,0+0) 02 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 tracking idé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 S o (ex : échiquier vide) pour aller vers une situation finale S f (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 S f. Le passage de la situation S i à S i+1 se fait par une action. Pour chaque situation S i 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 S i on a le choix entre plusieurs actions menant à des situations éventuellement différentes. On appelle chemin une suite d ’actions :
Fonction AES (s:Situation) : (Chemin,Situation) Variables trouvé, choix si 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 choix choix <-choix - {action} (chemin,situation)<- (AES (successeur(s,action))) fin tant que si trouvé alors ajouter(action, chemin),sf)) sinon retourner (Erreur Chemin, Erreur action); 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
Programmation du trajet d’un robot mobile #define Y 9 /* Les dimensions du labyrinthe */ #define X 7 char 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 A B S C D E F E G H I J
Retour en arrière - réponse A B C D E F G H I J X E S oo o o ooX X o o o o o ooo o oX o oo o ooo o o o oo 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 (h 2 (x)) ] ] c(h 2 (x)) =>u [ g(x), u [ g(h(x)),f(h 2 (x)) ] ] Version itérative ! c(x)=> p=g(x); x=h(x); ! c(h(x))=>p = u [ g(x), g(h(x))] x= h 2 (x) c(h 2 (x)) => u [u [ g(x), g(h(x))],f(h 2 (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))] A0 On remonte A1 P [F2(F1(x))] A2 A0 On remonte A3 On 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 = =1 A0(x) : deplacer (A,B) A1(x) : action vide F1(x) : permuter(B,C); n=n-1; A2(x) : deplacer (A,B) F2(x) : permuter(A,C); n=n-1; A3(x) : action vide mm=2 AAm+1 : action vide DDernier appel terminal =>Economie mémoire (pile)
Transformation : Hanoi void Hanoi (int n,...) { empiler (x,1) REPETER { TANT QUE (non c(x)) FAIRE //action vide empiler (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