Chapitre 12 — Récursivité
Résoudre un problème par des appels contrôlés à soi-même
Fiche pédagogique du chapitre
Objectifs d’apprentissage
À la fin de ce chapitre, l’étudiant devra être capable de :
- définir la récursivité et reconnaître une fonction qui s’appelle elle-même ;
- identifier le cas de base, le cas récursif et la mesure de progression ;
- expliquer la descente des appels et la remontée des résultats ;
- représenter l’empilement et le dépilement des appels dans une pile d’exécution ;
- tracer manuellement l’exécution d’un algorithme récursif ;
- transformer, lorsque cela est pertinent, une solution récursive en solution itérative ;
- comparer les avantages, les limites et les coûts des deux approches ;
- prévenir la récursion infinie et les dépassements de pile ;
- concevoir les fonctions récursives de factorielle, somme, puissance et PGCD ;
- analyser la récursivité naïve de la suite de Fibonacci ;
- effectuer une recherche récursive dans un tableau ;
- tester un algorithme récursif avec des cas normaux, limites et incorrects.
Prérequis
- maîtriser les variables, les expressions et les structures conditionnelles ;
- savoir utiliser les boucles Pour et TantQue ;
- connaître les fonctions, les paramètres et les valeurs de retour ;
- savoir manipuler un tableau à une dimension ;
- être capable de construire une table de trace.
Plan du chapitre
12.1 Principe de la récursivité
12.2 Récursivité et itération
12.3 Exemples classiques
Applications, traçage, travaux dirigés, corrigés et synthèse
Organisation pédagogique indicative
Activité | Durée indicative | Objectif principal |
|---|---|---|
| Cours | 3 h | Comprendre les appels récursifs, le cas de base et la pile d’exécution. |
| Travaux dirigés | 3 h | Tracer les appels et construire des récurrences correctes. |
| Travaux pratiques | 3 h | Implémenter et tester plusieurs fonctions récursives. |
| Travail personnel | 2 h | Comparer les versions récursives et itératives. |
| Idée centrale : une fonction récursive résout un problème en le réduisant à une instance plus petite du même problème. La réduction doit obligatoirement conduire à un cas de base qui arrête les appels. |
Introduction à la récursivité
La récursivité est une technique de résolution dans laquelle un sous-programme utilise sa propre définition. Au lieu de répéter explicitement des instructions avec une boucle, la fonction délègue une partie du travail à un nouvel appel portant sur une donnée plus petite ou plus simple.
Cette technique apparaît naturellement dans les problèmes définis par récurrence, dans les structures hiérarchiques et dans les stratégies de division d’un problème. Elle est particulièrement utile lorsque la solution d’un cas dépend directement de la solution d’un cas plus petit.
Cependant, une définition récursive correcte ne consiste pas seulement à écrire un auto-appel. Elle doit préciser une situation terminale, garantir une progression vers cette situation et combiner correctement le résultat obtenu lors de la remontée des appels.
| Principe de sécurité : avant d’écrire un appel récursif, identifier le cas de base et prouver que chaque appel se rapproche de ce cas. |
12.1 Principe de la récursivité
12.1.1 Fonction qui s’appelle elle-même
Une fonction est dite récursive lorsqu’elle s’appelle directement ou indirectement pendant son exécution. Dans la récursivité directe, la fonction contient un appel à son propre nom. Dans la récursivité indirecte, une fonction A appelle une fonction B qui finit par rappeler A.
Dans ce chapitre, les exemples utilisent principalement la récursivité directe, plus simple à observer et à tracer.
Structure générale d’une fonction récursive
| Fonction Traiter(probleme) : TypeResultat Début Si CasDeBase(probleme) Alors Retourner ResultatSimple Sinon sousProbleme ← Reduire(probleme) resultatPartiel ← Traiter(sousProbleme) Retourner Combiner(probleme, resultatPartiel) FinSi Fin |
Élément | Rôle | Question à se poser |
|---|---|---|
| Paramètre | Décrit l’instance courante du problème. | Quelle donnée diminue ou se simplifie ? |
| Cas de base | Fournit un résultat sans nouvel appel. | Quand puis-je répondre directement ? |
| Appel récursif | Résout une instance plus petite. | Le nouvel argument se rapproche-t-il du cas de base ? |
| Combinaison | Construit le résultat courant à partir du résultat partiel. | Que faire du résultat retourné ? |
12.1.2 Exemple introductif : compte à rebours
L’exemple suivant affiche les entiers de N à 1. Le cas N = 0 arrête la récursion. Pour N > 0, la fonction affiche N puis s’appelle avec N - 1.
Pseudo-code
| Procédure CompteARebours(n : Entier) Début Si n = 0 Alors Afficher("Terminé") Sinon Afficher(n) CompteARebours(n - 1) FinSi Fin |
Appel courant | Action avant l’appel | Appel suivant |
|---|---|---|
| CompteARebours(3) | Afficher 3 | CompteARebours(2) |
| CompteARebours(2) | Afficher 2 | CompteARebours(1) |
| CompteARebours(1) | Afficher 1 | CompteARebours(0) |
| CompteARebours(0) | Afficher « Terminé » | Aucun appel |
| Observation : les instructions placées avant l’appel récursif s’exécutent pendant la descente. Celles placées après l’appel s’exécutent pendant la remontée. |
12.1.3 Cas de base
Le cas de base est une situation assez simple pour être résolue immédiatement. Il empêche la fonction de s’appeler indéfiniment. Une fonction peut posséder un seul cas de base ou plusieurs cas de base.
- Factorielle : 0! = 1 constitue le cas de base.
- Fibonacci : F(0) = 0 et F(1) = 1 constituent deux cas de base.
- Recherche dans un tableau : atteindre la fin du tableau constitue un cas d’échec.
- Recherche dichotomique : l’intervalle vide constitue un cas d’échec.
| Attention : un cas de base présent dans le code ne suffit pas. Il doit être effectivement atteignable par tous les appels valides. |
12.1.4 Cas récursif
Le cas récursif exprime la solution du problème courant à partir d’un ou plusieurs problèmes plus petits. Il comporte généralement une transformation des paramètres, un appel récursif et une combinaison du résultat.
Pour calculer la somme des entiers de 1 à N, on peut utiliser la relation Somme(N) = N + Somme(N - 1). La valeur de N diminue à chaque appel, jusqu’à N = 0.
Exemple minimal
| Fonction Somme(n : Entier) : Entier Début Si n = 0 Alors Retourner 0 Sinon Retourner n + Somme(n - 1) FinSi Fin |
12.1.5 Mesure de progression et terminaison
Pour justifier qu’un algorithme récursif se termine, on choisit une mesure entière et non négative qui diminue strictement à chaque appel. Cette mesure peut être la valeur de N, la taille d’un intervalle, le nombre d’éléments restant à traiter ou la hauteur d’une structure.
Problème | Mesure de progression | Évolution |
|---|---|---|
| Factorielle de n | n | n devient n - 1 |
| PGCD(a, b) | valeur du second argument | b devient a modulo b, inférieur à b |
| Recherche séquentielle | nombre d’éléments non examinés | l’indice augmente |
| Recherche dichotomique | taille de l’intervalle | l’intervalle est approximativement divisé par deux |
Une preuve élémentaire de terminaison suit donc deux idées : la mesure ne peut pas décroître indéfiniment et le cas de base est atteint lorsque cette mesure prend sa valeur minimale.
12.1.6 Empilement des appels
Chaque appel de fonction possède son propre contexte d’exécution : paramètres, variables locales, adresse de retour et état des calculs en attente. Ces contextes sont mémorisés dans une pile d’appels. Le dernier appel créé est le premier à se terminer, selon le principe LIFO : dernier entré, premier sorti.
Étape | Sommet de la pile | Calcul en attente |
|---|---|---|
| 1 | Factorielle(4) | 4 × Factorielle(3) |
| 2 | Factorielle(3) | 3 × Factorielle(2) |
| 3 | Factorielle(2) | 2 × Factorielle(1) |
| 4 | Factorielle(1) | 1 × Factorielle(0) |
| 5 | Factorielle(0) | Retourner 1 |
| 6 | Dépilement | 1 × 1 = 1, puis 2 × 1 = 2, puis 3 × 2 = 6, puis 4 × 6 = 24 |
La phase de descente crée les appels. La phase de remontée restitue les résultats dans l’ordre inverse. Une variable locale appartenant à un appel n’est pas confondue avec la variable locale portant le même nom dans un autre appel.
12.1.7 Arbre des appels
Lorsqu’un appel récursif produit plusieurs nouveaux appels, la trace prend la forme d’un arbre. La suite de Fibonacci en fournit un exemple : Fibonacci(5) appelle Fibonacci(4) et Fibonacci(3), chacun générant à son tour d’autres appels.
Arbre partiel de Fibonacci(5)
| F(5) ├── F(4) │ ├── F(3) │ │ ├── F(2) │ │ └── F(1) │ └── F(2) └── F(3) ├── F(2) └── F(1) |
Cet arbre met en évidence des calculs répétés. Par exemple, F(3) et F(2) sont recalculés plusieurs fois. Une formulation récursive élégante peut donc être inefficace si elle duplique beaucoup de travail.
12.1.8 Contrat d’une fonction récursive
Un contrat précise les conditions d’utilisation et le résultat garanti. Il est particulièrement important en récursivité, car un paramètre incorrect peut empêcher d’atteindre le cas de base.
Élément du contrat | Exemple pour Factorielle(n) |
|---|---|
| Précondition | n est un entier supérieur ou égal à 0. |
| Postcondition | la fonction retourne le produit des entiers de 1 à n. |
| Cas de base | si n = 0, retourner 1. |
| Progression | l’appel utilise n - 1. |
| Invariant attendu | chaque appel reçoit un entier non négatif. |
12.2 Récursivité et itération
12.2.1 Solution récursive
Une solution récursive exprime le problème à l’aide d’une relation entre une instance et une instance plus petite. Elle met l’accent sur la définition du problème plutôt que sur la gestion explicite de la répétition.
Factorielle récursive
| Fonction FactorielleRec(n : Entier) : Entier Début Si n = 0 Alors Retourner 1 Sinon Retourner n × FactorielleRec(n - 1) FinSi Fin |
12.2.2 Solution itérative
Une solution itérative utilise une boucle et des variables d’état. Elle contrôle explicitement la progression et ne crée pas une nouvelle pile d’appels pour chaque répétition.
Factorielle itérative
| Fonction FactorielleIter(n : Entier) : Entier Variables i, produit : Entier Début produit ← 1 Pour i allant de 2 à n Faire produit ← produit × i FinPour Retourner produit Fin |
12.2.3 Comparaison des deux approches
Critère | Récursivité | Itération |
|---|---|---|
| Expression du problème | Souvent proche de la définition mathématique. | Souvent centrée sur l’évolution des variables. |
| Mémoire | Utilise un contexte par appel. | Utilise généralement un nombre fixe de variables. |
| Lisibilité | Très claire pour les problèmes naturellement récursifs. | Très claire pour les répétitions simples. |
| Risque principal | Récursion infinie ou dépassement de pile. | Boucle infinie ou mauvaise mise à jour. |
| Coût des appels | Un coût supplémentaire peut exister à chaque appel. | Pas de coût d’appel récursif. |
| Transformation | Peut parfois être remplacée par une boucle ou une pile explicite. | Peut parfois être reformulée par récurrence. |
12.2.4 Avantages de la récursivité
- elle traduit directement certaines définitions mathématiques ;
- elle simplifie le traitement des problèmes divisés en sous-problèmes de même nature ;
- elle rend naturelle l’exploration d’arbres, de dossiers ou de structures imbriquées ;
- elle réduit parfois le nombre de variables de contrôle visibles ;
- elle facilite certains raisonnements par induction.
12.2.5 Limites de la récursivité
- chaque appel consomme de la mémoire dans la pile d’exécution ;
- une profondeur trop grande peut provoquer un dépassement de pile ;
- les appels répétés peuvent rendre une solution lente ;
- le traçage peut devenir difficile lorsque plusieurs branches sont créées ;
- une mauvaise progression peut produire une récursion infinie ;
- certains langages limitent explicitement la profondeur des appels.
| Choix raisonné : la récursivité n’est pas automatiquement meilleure qu’une boucle. Elle doit apporter une formulation plus naturelle ou une simplification réelle sans coût excessif. |
12.2.6 Risque de récursion infinie
Une récursion infinie apparaît lorsque le cas de base n’est jamais atteint. Cela peut provenir d’un cas de base absent, d’une condition erronée ou d’un argument qui ne progresse pas.
Exemple incorrect
| Fonction SommeIncorrecte(n : Entier) : Entier Début Si n = 0 Alors Retourner 0 Sinon Retourner n + SommeIncorrecte(n + 1) // n s’éloigne de 0 FinSi Fin |
Erreur | Conséquence | Correction |
|---|---|---|
| Cas de base impossible | Les appels ne s’arrêtent pas. | Adapter le cas de base au domaine réel. |
| Argument inchangé | Le même appel se répète. | Modifier strictement la mesure de progression. |
| Progression dans le mauvais sens | La distance au cas de base augmente. | Vérifier le signe de la mise à jour. |
| Donnée hors précondition | Le chemin prévu n’est pas suivi. | Valider les paramètres avant l’appel. |
12.2.7 Récursivité terminale
Une récursion est dite terminale lorsque l’appel récursif constitue la dernière opération exécutée par la fonction. Aucun calcul n’attend le retour de l’appel. Certains compilateurs ou interpréteurs peuvent optimiser ce cas, mais cette optimisation ne doit pas être supposée sans connaître le langage utilisé.
Factorielle avec accumulateur
| Fonction FactAux(n, accumulateur : Entier) : Entier Début Si n = 0 Alors Retourner accumulateur Sinon Retourner FactAux(n - 1, n × accumulateur) FinSi Fin Fonction Factorielle(n : Entier) : Entier Début Retourner FactAux(n, 1) Fin |
Dans un cours d’algorithmique, cette forme sert surtout à comprendre le rôle d’un accumulateur et la transformation possible vers une boucle.
12.2.8 Mémoïsation et calculs répétés
La mémoïsation consiste à mémoriser les résultats déjà calculés afin de les réutiliser. Elle est utile lorsqu’un arbre récursif contient de nombreux sous-problèmes identiques, comme dans la version naïve de Fibonacci.
Cette technique échange de la mémoire contre du temps de calcul. Elle sera étudiée plus en profondeur dans les chapitres consacrés à l’optimisation ou à la programmation dynamique.
12.2.9 Comment choisir ?
Situation | Choix généralement adapté |
|---|---|
| Répéter un traitement un nombre connu de fois | Boucle Pour |
| Accumuler une somme ou un produit simple | Boucle, sauf objectif pédagogique |
| Définition mathématique par récurrence | Récursivité possible |
| Diviser un intervalle en sous-intervalles | Récursivité souvent naturelle |
| Explorer une structure hiérarchique | Récursivité souvent naturelle |
| Profondeur potentiellement très grande | Itération ou pile explicite à envisager |
| Sous-problèmes répétés | Mémoïsation ou version itérative |
12.3 Exemples classiques
12.3.1 Factorielle
Pour un entier n supérieur ou égal à 0, la factorielle n! est le produit des entiers de 1 à n. Par convention, 0! = 1.
Définition | Valeur |
|---|---|
| Cas de base | Factorielle(0) = 1 |
| Cas récursif | Factorielle(n) = n × Factorielle(n - 1), pour n > 0 |
Algorithme
| Fonction Factorielle(n : Entier) : Entier Début Si n < 0 Alors Erreur("La factorielle exige n ≥ 0") FinSi Si n = 0 Alors Retourner 1 Sinon Retourner n × Factorielle(n - 1) FinSi Fin |
Expression | Réduction |
|---|---|
| Factorielle(4) | 4 × Factorielle(3) |
| Factorielle(3) | 3 × Factorielle(2) |
| Factorielle(2) | 2 × Factorielle(1) |
| Factorielle(1) | 1 × Factorielle(0) |
| Factorielle(0) | 1 |
| Remontée | 1, puis 2, puis 6, puis 24 |
Complexité : la fonction réalise n appels récursifs, donc son temps est O(n) et la profondeur de pile est O(n). Les valeurs augmentent très rapidement ; le type entier peut déborder pour des valeurs relativement petites.
12.3.2 Somme des nombres de 1 à N
La somme S(n) = 1 + 2 + ... + n peut être définie par S(0) = 0 et S(n) = n + S(n - 1).
Algorithme récursif
| Fonction SommeEntiers(n : Entier) : Entier Début Si n < 0 Alors Erreur("n doit être positif ou nul") FinSi Si n = 0 Alors Retourner 0 Sinon Retourner n + SommeEntiers(n - 1) FinSi Fin |
Appel | Valeur retournée |
|---|---|
| SommeEntiers(0) | 0 |
| SommeEntiers(1) | 1 + 0 = 1 |
| SommeEntiers(2) | 2 + 1 = 3 |
| SommeEntiers(3) | 3 + 3 = 6 |
| SommeEntiers(4) | 4 + 6 = 10 |
Cette solution est utile pour comprendre la récursivité, mais une formule directe n(n + 1) / 2 ou une boucle utilise moins de pile.
12.3.3 Suite de Fibonacci
La suite de Fibonacci est définie par F(0) = 0, F(1) = 1 et F(n) = F(n - 1) + F(n - 2) pour n ≥ 2.
Version récursive naïve
| Fonction Fibonacci(n : Entier) : Entier Début Si n < 0 Alors Erreur("n doit être positif ou nul") FinSi Si n = 0 Alors Retourner 0 SinonSi n = 1 Alors Retourner 1 Sinon Retourner Fibonacci(n - 1) + Fibonacci(n - 2) FinSi Fin |
n | F(n) | Remarque |
|---|---|---|
| 0 | 0 | Cas de base |
| 1 | 1 | Cas de base |
| 2 | 1 | F(1) + F(0) |
| 3 | 2 | F(2) + F(1) |
| 4 | 3 | F(3) + F(2) |
| 5 | 5 | F(4) + F(3) |
La version naïve est simple mais recalcule les mêmes valeurs. Son nombre d’appels croît très rapidement et son temps est exponentiel. La profondeur de pile reste linéaire, mais le nombre total d’appels est beaucoup plus élevé.
Version itérative efficace
| Fonction FibonacciIter(n : Entier) : Entier Variables a, b, suivant, i : Entier Début a ← 0 b ← 1 Pour i allant de 1 à n Faire suivant ← a + b a ← b b ← suivant FinPour Retourner a Fin |
| Leçon algorithmique : une définition récursive naturelle ne garantit pas une exécution efficace. Il faut repérer les sous-calculs répétés et choisir une stratégie adaptée. |
12.3.4 Calcul d’une puissance
Pour un réel x et un entier naturel n, la puissance x^n peut être définie par x^0 = 1 et x^n = x × x^(n - 1).
Version récursive simple
| Fonction Puissance(x : Réel, n : Entier) : Réel Début Si n < 0 Alors Erreur("Cette version exige n ≥ 0") FinSi Si n = 0 Alors Retourner 1 Sinon Retourner x × Puissance(x, n - 1) FinSi Fin |
Cette version effectue n multiplications. Une version plus efficace utilise la parité de n : si n est pair, x^n = (x^(n/2))² ; si n est impair, x^n = x × x^(n - 1).
Exponentiation rapide
| Fonction PuissanceRapide(x : Réel, n : Entier) : Réel Variables demi : Réel Début Si n = 0 Alors Retourner 1 FinSi demi ← PuissanceRapide(x, n DIV 2) Si n MOD 2 = 0 Alors Retourner demi × demi Sinon Retourner x × demi × demi FinSi Fin |
Algorithme | Nombre approximatif d’appels | Complexité |
|---|---|---|
| Puissance simple | n | O(n) |
| Puissance rapide | log2(n) | O(log n) |
12.3.5 PGCD d’Euclide
Le plus grand commun diviseur de deux entiers peut être calculé par l’algorithme d’Euclide. La relation essentielle est PGCD(a, b) = PGCD(b, a MOD b), avec le cas de base PGCD(a, 0) = |a|.
Algorithme récursif
| Fonction PGCD(a, b : Entier) : Entier Début a ← ValeurAbsolue(a) b ← ValeurAbsolue(b) Si b = 0 Alors Retourner a Sinon Retourner PGCD(b, a MOD b) FinSi Fin |
Appel | Calcul du reste | Appel suivant |
|---|---|---|
| PGCD(48, 18) | 48 MOD 18 = 12 | PGCD(18, 12) |
| PGCD(18, 12) | 18 MOD 12 = 6 | PGCD(12, 6) |
| PGCD(12, 6) | 12 MOD 6 = 0 | PGCD(6, 0) |
| PGCD(6, 0) | Cas de base | Retourner 6 |
Le second argument diminue strictement tant qu’il est non nul. La profondeur est logarithmique dans les valeurs des entrées, ce qui rend cet algorithme très efficace.
12.3.6 Recherche récursive dans un tableau
Une recherche séquentielle récursive examine l’élément d’indice i. Si la valeur est trouvée, elle retourne i. Si i atteint la taille du tableau, elle retourne -1. Sinon, elle continue avec i + 1.
Recherche séquentielle récursive
| Fonction RechercherRec(T : Tableau, n, valeur, i : Entier) : Entier Début Si i = n Alors Retourner -1 SinonSi T[i] = valeur Alors Retourner i Sinon Retourner RechercherRec(T, n, valeur, i + 1) FinSi Fin // Appel initial : RechercherRec(T, n, valeur, 0) |
Tableau | Valeur | Indices examinés | Résultat |
|---|---|---|---|
| [7, 4, 9, 2] | 9 | 0, 1, 2 | 2 |
| [7, 4, 9, 2] | 7 | 0 | 0 |
| [7, 4, 9, 2] | 5 | 0, 1, 2, 3, fin | -1 |
| [] | 3 | fin immédiate | -1 |
Le temps est O(n) dans le pire cas et la profondeur de pile est également O(n). Pour un tableau trié, une recherche dichotomique récursive peut réduire le temps à O(log n).
Extension : recherche dichotomique récursive
| Fonction RechercheDicho(T, valeur, gauche, droite : Entier) : Entier Variables milieu : Entier Début Si gauche > droite Alors Retourner -1 FinSi milieu ← (gauche + droite) DIV 2 Si T[milieu] = valeur Alors Retourner milieu SinonSi valeur < T[milieu] Alors Retourner RechercheDicho(T, valeur, gauche, milieu - 1) Sinon Retourner RechercheDicho(T, valeur, milieu + 1, droite) FinSi Fin |
Traçage des appels récursifs
Le traçage manuel doit distinguer les appels créés, les paramètres propres à chaque appel et les valeurs retournées. Une méthode fiable consiste à numéroter les appels et à noter les opérations en attente.
Méthode de traçage
- Écrire l’appel initial avec les valeurs de ses paramètres.
- Évaluer la condition du cas de base.
- Si le cas récursif est choisi, noter l’opération en attente.
- Écrire le nouvel appel avec ses nouveaux paramètres.
- Répéter jusqu’au cas de base.
- Remonter les appels dans l’ordre inverse et calculer chaque valeur retournée.
- Vérifier que le résultat final correspond au jeu d’essai.
Niveau | Appel | Opération en attente | Retour |
|---|---|---|---|
| 1 | Somme(3) | 3 + Somme(2) | 6 |
| 2 | Somme(2) | 2 + Somme(1) | 3 |
| 3 | Somme(1) | 1 + Somme(0) | 1 |
| 4 | Somme(0) | Aucune | 0 |
Erreurs fréquentes et bonnes pratiques
Erreur fréquente | Pourquoi elle est dangereuse | Bonne pratique |
|---|---|---|
| Oublier le cas de base | La fonction ne possède aucune sortie terminale. | Écrire le cas de base avant l’appel récursif. |
| Appeler avec le même argument | Aucune progression n’est réalisée. | Identifier une mesure qui diminue strictement. |
| Modifier le mauvais paramètre | Le cas de base peut devenir inaccessible. | Tracer trois appels à la main avant de valider. |
| Confondre affichage et retour | Le résultat ne peut pas être combiné. | Retourner une valeur lorsqu’un calcul dépend du résultat. |
| Dupliquer des appels coûteux | Le temps peut devenir exponentiel. | Mémoriser les résultats ou utiliser une version itérative. |
| Ignorer les entrées invalides | Le domaine de la récurrence n’est plus respecté. | Définir et contrôler les préconditions. |
| Utiliser une profondeur excessive | La pile peut être saturée. | Préférer une boucle ou une pile explicite si nécessaire. |
Checklist avant validation
- Le cas de base couvre-t-il les plus petites entrées valides ?
- L’appel récursif reçoit-il une instance strictement plus petite ?
- Tous les chemins d’exécution finissent-ils par retourner une valeur ?
- Les opérations en attente combinent-elles correctement les résultats ?
- Les entrées invalides sont-elles refusées ou traitées ?
- La profondeur maximale reste-t-elle raisonnable ?
- Les mêmes sous-problèmes sont-ils recalculés inutilement ?
- Des jeux d’essai limites ont-ils été tracés ?
Travaux dirigés
Pour chaque exercice, préciser le cas de base, le cas récursif, la mesure de progression, le pseudo-code et au moins trois jeux d’essai.
Exercice 1 — Affichage descendant et ascendant
Écrire une procédure récursive qui reçoit N et affiche d’abord les entiers de N à 1, puis les entiers de 1 à N. Expliquer la position des instructions d’affichage par rapport à l’appel récursif.
Exercice 2 — Somme des chiffres
Écrire une fonction récursive qui calcule la somme des chiffres d’un entier positif. Exemple : SommeChiffres(472) = 13.
Exercice 3 — Nombre de chiffres
Écrire une fonction récursive qui détermine le nombre de chiffres d’un entier positif. Préciser le traitement de la valeur 0.
Exercice 4 — Inversion d’une chaîne
Écrire une fonction récursive qui retourne l’inverse d’une chaîne. Exemple : Inverser("ALGO") retourne "OGLA".
Exercice 5 — Palindrome
Écrire une fonction récursive qui vérifie si une chaîne est un palindrome en comparant les caractères situés aux deux extrémités.
Exercice 6 — Minimum d’un tableau
Écrire une fonction récursive qui retourne la plus petite valeur des n premiers éléments d’un tableau non vide.
Exercice 7 — Recherche dichotomique
Écrire et tracer une recherche dichotomique récursive dans le tableau trié [2, 5, 8, 12, 16, 23, 38]. Rechercher successivement 16, 2 et 10.
Exercice 8 — Conversion décimale vers binaire
Écrire une procédure récursive qui affiche la représentation binaire d’un entier positif en utilisant la division entière par 2 et le modulo 2.
Exercice 9 — Tours de Hanoï
Trois tiges A, B et C contiennent des disques de tailles différentes. Écrire la stratégie récursive permettant de déplacer N disques de A vers C en utilisant B, sans poser un grand disque sur un plus petit.
Exercice 10 — Diagnostic d’algorithmes
Pour chacun des algorithmes suivants, indiquer s’il se termine et proposer une correction si nécessaire :
| a) F(n) : si n = 0 retourner 0 sinon retourner F(n) b) G(n) : si n ≤ 0 retourner 1 sinon retourner n × G(n - 2) c) H(n) : si n = 1 retourner 1 sinon retourner H(n / 2) d) R(T, i) : si T[i] = x retourner i sinon retourner R(T, i + 1) |
Corrigés indicatifs
Corrigé 1 — Affichage descendant et ascendant
| Procédure Descendant(n : Entier) Début Si n > 0 Alors Afficher(n) Descendant(n - 1) FinSi Fin Procédure Ascendant(n : Entier) Début Si n > 0 Alors Ascendant(n - 1) Afficher(n) FinSi Fin |
Dans la première procédure, l’affichage précède l’appel : il se produit pendant la descente. Dans la seconde, il suit l’appel : il se produit pendant la remontée.
Corrigé 2 — Somme des chiffres
| Fonction SommeChiffres(n : Entier) : Entier Début n ← ValeurAbsolue(n) Si n < 10 Alors Retourner n Sinon Retourner (n MOD 10) + SommeChiffres(n DIV 10) FinSi Fin |
La division entière par 10 supprime le dernier chiffre. La mesure de progression est le nombre de chiffres restant.
Corrigé 3 — Nombre de chiffres
| Fonction NombreChiffres(n : Entier) : Entier Début n ← ValeurAbsolue(n) Si n < 10 Alors Retourner 1 Sinon Retourner 1 + NombreChiffres(n DIV 10) FinSi Fin |
La valeur 0 contient un chiffre, donc le cas n < 10 retourne 1.
Corrigé 4 — Inversion d’une chaîne
| Fonction Inverser(chaine : Chaîne) : Chaîne Début Si Longueur(chaine) ≤ 1 Alors Retourner chaine Sinon Retourner DernierCaractere(chaine) + Inverser(SousChaineSansDernier(chaine)) FinSi Fin |
Une autre solution place le premier caractère après l’inversion du reste. L’important est que la chaîne transmise à l’appel soit plus courte.
Corrigé 5 — Palindrome
| Fonction EstPalindrome(chaine : Chaîne, gauche, droite : Entier) : Booléen Début Si gauche ≥ droite Alors Retourner Vrai SinonSi chaine[gauche] ≠ chaine[droite] Alors Retourner Faux Sinon Retourner EstPalindrome(chaine, gauche + 1, droite - 1) FinSi Fin |
L’appel initial utilise gauche = 0 et droite = Longueur(chaine) - 1.
Corrigé 6 — Minimum d’un tableau
| Fonction MinimumRec(T : Tableau, n : Entier) : Réel Variables minReste : Réel Début Si n = 1 Alors Retourner T[0] FinSi minReste ← MinimumRec(T, n - 1) Si T[n - 1] < minReste Alors Retourner T[n - 1] Sinon Retourner minReste FinSi Fin |
Précondition : n ≥ 1. Chaque appel traite un préfixe contenant un élément de moins.
Corrigé 7 — Recherche dichotomique
Pour rechercher 16, le milieu initial contient 12, puis la recherche continue à droite et trouve 16. Pour rechercher 2, elle continue à gauche jusqu’au premier élément. Pour 10, l’intervalle finit par devenir vide et la fonction retourne -1.
| Fonction RechercheDicho(T, x, g, d : Entier) : Entier Variables m : Entier Début Si g > d Alors Retourner -1 FinSi m ← (g + d) DIV 2 Si T[m] = x Alors Retourner m SinonSi x < T[m] Alors Retourner RechercheDicho(T, x, g, m - 1) Sinon Retourner RechercheDicho(T, x, m + 1, d) FinSi Fin |
Corrigé 8 — Conversion décimale vers binaire
| Procédure AfficherBinaire(n : Entier) Début Si n ≥ 2 Alors AfficherBinaire(n DIV 2) FinSi AfficherSansRetourLigne(n MOD 2) Fin |
L’appel récursif est effectué avant l’affichage afin que les bits de poids fort soient affichés en premier. Pour n = 0, on peut traiter séparément l’appel initial et afficher 0.
Corrigé 9 — Tours de Hanoï
| Procédure Hanoi(n : Entier, depart, intermediaire, arrivee : Caractère) Début Si n = 1 Alors Afficher("Déplacer un disque de ", depart, " vers ", arrivee) Sinon Hanoi(n - 1, depart, arrivee, intermediaire) Afficher("Déplacer un disque de ", depart, " vers ", arrivee) Hanoi(n - 1, intermediaire, depart, arrivee) FinSi Fin |
Le nombre minimal de déplacements est 2^n - 1. L’exercice illustre une récursion à deux branches et une croissance exponentielle du nombre d’actions.
Corrigé 10 — Diagnostic d’algorithmes
Algorithme | Diagnostic | Correction ou condition |
|---|---|---|
| F(n) | Ne progresse jamais : F(n) rappelle F(n). | Utiliser un argument plus petit, par exemple F(n - 1). |
| G(n) | Se termine pour n positif, car n diminue de 2 et le cas n ≤ 0 couvre les deux parités. | Valider le domaine et vérifier le résultat attendu. |
| H(n) | Le cas n = 1 est insuffisant pour n = 0 ; la division dépend aussi du type. | Utiliser n ≤ 1 et une division entière. |
| R(T, i) | Peut dépasser la fin du tableau si x est absent. | Tester i = taille avant d’accéder à T[i]. |
Synthèse du chapitre
La récursivité permet à une fonction de résoudre un problème en appelant une version plus petite d’elle-même. Une solution récursive fiable repose sur un cas de base, un cas récursif, une progression démontrable et une combinaison correcte des résultats.
Notion | À retenir |
|---|---|
| Cas de base | Il retourne directement un résultat et arrête les appels. |
| Cas récursif | Il réduit le problème et appelle la fonction sur une instance plus petite. |
| Pile d’appels | Elle conserve les paramètres, variables locales et opérations en attente. |
| Descente | Les appels sont créés jusqu’au cas de base. |
| Remontée | Les résultats sont retournés dans l’ordre inverse. |
| Terminaison | Une mesure non négative doit diminuer strictement. |
| Coût | La récursivité peut consommer plus de mémoire ou répéter des calculs. |
| Choix | Utiliser la formulation la plus claire et la plus adaptée aux contraintes. |
Checklist de conception d’une fonction récursive
1. Définir précisément le problème et le domaine des entrées.
2. Identifier la plus petite instance résoluble directement.
3. Écrire le ou les cas de base.
4. Choisir une mesure de progression.
5. Construire un appel avec une instance strictement plus petite.
6. Déterminer comment combiner le résultat récursif avec le cas courant.
7. Tracer plusieurs appels, y compris les cas limites.
8. Évaluer la profondeur de pile et les calculs répétés.
9. Comparer avec une solution itérative lorsque cela est pertinent.
10. Tester les entrées incorrectes et documenter les préconditions.
Glossaire
Terme | Définition |
|---|---|
| Récursivité | Technique dans laquelle un sous-programme s’appelle lui-même directement ou indirectement. |
| Cas de base | Cas résolu sans nouvel appel récursif. |
| Cas récursif | Cas qui réduit le problème et effectue un nouvel appel. |
| Pile d’appels | Structure mémorisant les contextes des appels actifs. |
| Profondeur | Nombre maximal d’appels simultanément présents dans la pile. |
| Descente | Phase de création successive des appels. |
| Remontée | Phase de retour et de combinaison des résultats. |
| Récursion terminale | Récursion dont l’appel récursif est la dernière opération. |
| Mémoïsation | Mémorisation de résultats déjà calculés pour éviter les répétitions. |
| Dépassement de pile | Erreur provoquée par une profondeur d’appels supérieure à la capacité disponible. |
Auto-évaluation
Compétence | Acquise | À renforcer |
|---|---|---|
| Je sais identifier le cas de base et le cas récursif. | □ | □ |
| Je sais expliquer l’empilement et le dépilement des appels. | □ | □ |
| Je sais tracer une fonction récursive simple. | □ | □ |
| Je sais vérifier qu’un argument progresse vers le cas de base. | □ | □ |
| Je sais comparer une solution récursive et une solution itérative. | □ | □ |
| Je sais écrire Factorielle, Fibonacci, Puissance et PGCD. | □ | □ |
| Je sais effectuer une recherche récursive dans un tableau. | □ | □ |
| Je sais détecter une récursion infinie ou inefficace. | □ | □ |
| Conclusion : la récursivité est une manière de penser avant d’être une syntaxe. Sa maîtrise repose sur la capacité à réduire un problème, à contrôler la terminaison et à raisonner sur la pile des appels. |