Programmer par récursivité

Comprendre les appels récursifs, leurs cas de base, leur terminaison et leur coût à partir de programmes Python.

Environ 40 minutes

  • Écrire une fonction récursive
  • Justifier sa terminaison
  • Tracer les valeurs renvoyées
Sommaire
  1. Définir le problème avant les appels
  2. Prouver la terminaison et la correction
  3. Appliquer la récursivité à un autre domaine
  4. Comparer à une boucle et tester
  5. Erreurs fréquentes et bilan

Comment résoudre un problème en confiant une partie du travail à la même fonction ?

Une fonction peut appeler une autre fonction ; elle peut aussi se rappeler elle-même. Cette idée devient utile quand une réponse se déduit de la réponse à une instance plus petite. Nous suivrons les appels, justifierons leur terminaison et comparerons leur coût. Prérequis : paramètres, valeur renvoyée, conditions, chaînes et boucles en Python. Les entrées sont finies et respectent les domaines indiqués ; un dépassement des ressources de la machine reste possible.

Définir le problème avant les appels

Une définition récursive relie un problème à une ou plusieurs instances du même problème. Elle comprend des cas de base, traités directement, et une règle de réduction. Le cas de base ne signifie pas que tout le programme disparaît : il produit une valeur qui permet aux appels en attente de poursuivre leur calcul. Chaque appel possède ses propres paramètres et ses propres variables locales.

Prenons la somme des entiers de 1 à n, pour n entier naturel. La somme vide, obtenue pour n égal à zéro, vaut zéro. Pour n strictement positif, on ajoute n à la somme jusqu’à n − 1. Le contrat précise l’entrée et la sortie ; il donne aussi la précondition n ≥ 0. Une fonction correcte pour les naturels ne devient pas automatiquement correcte pour un entier négatif.

Pour écrire Python, conserve les deux-points et l’indentation. La première ligne définit la fonction ; le premier return se trouve dans le bloc conditionnel. Le dernier return est aligné avec if, à l’intérieur de la fonction.

Python
def somme(n):
  if n == 0:
    return 0
  return n + somme(n - 1)

Les appels descendent de 3 à 0 ; les résultats remontent de 0 à 6.

Exemple résolu

Détermine somme(3), puis explique ce que les appels attendent.

  1. Étape 1 : L’appel somme(3) attend la valeur de somme(2) pour lui ajouter 3 ; somme(2) attend somme(1).
  2. Étape 2 : L’appel somme(1) demande somme(0), qui renvoie directement 0.
  3. Étape 3 : On remonte ensuite : somme(1) renvoie 1, somme(2) renvoie 3 et somme(3) renvoie 6.

Résultat : Le résultat est 6. Il y a quatre appels au total, mais un seul résultat final pour l’appel initial.

Piège à éviter : Confondre n avec une variable globale efface à tort les valeurs conservées par les appels en attente.

À toi d'essayer

La fonction somme est appelée avec zéro. Combien d’appels sont exécutés et quelle valeur est renvoyée ?

Voir la réponse

Un seul appel est exécuté. Il rencontre immédiatement le cas de base et renvoie 0 ; aucun appel avec −1 n’a lieu.

Prouver la terminaison et la correction

Pour prouver l’arrêt, cherche une quantité entière naturelle qui diminue strictement à chaque appel récursif. On l’appelle un variant. Pour somme, cette quantité est n. Si n est positif, n − 1 est encore naturel et strictement inférieur à n ; on atteint donc zéro après un nombre fini de réductions. La présence d’un if n’est pas, à elle seule, une preuve de terminaison : il faut montrer que sa branche terminale sera atteinte.

Avec une entrée négative, le même programme s’éloigne de zéro. Le contrat exclut cette entrée ; on peut aussi ajouter une vérification explicite au début du programme. Remplacer le test n == 0 par n <= 0 change le comportement sur les négatifs, mais ce choix doit être cohérent avec la spécification. Un correctif ne se choisit pas seulement parce qu’il fait disparaître une erreur observée.

La correction est une autre question : la fonction peut terminer et renvoyer une valeur fausse. Pour somme, le cas zéro est correct. Si l’appel plus petit calcule correctement la somme jusqu’à n − 1, l’ajout de n donne exactement la somme jusqu’à n. Ce raisonnement associe une base et une étape de transmission. Il ne faut pas supposer directement que l’appel initial est correct.

Au bac : justifier un programme récursif

  1. Énonce le domaine d’entrée, le résultat attendu et le cas de base avant de dérouler un exemple.
  2. Identifie un variant naturel ; montre sa stricte diminution et explique pourquoi les appels restent dans le domaine.
  3. Relie la valeur renvoyée au contrat : résultat direct dans la base, puis combinaison correcte des résultats plus petits.
  4. Pour une trace, sépare descente des appels et remontée des valeurs ; termine par une phrase répondant précisément à la question.

Exemple résolu

Une fonction pair retire 2 à un naturel n. Elle renvoie True pour zéro et False pour un. Justifie ses deux bases.

  1. Étape 1 : Un naturel peut atteindre zéro ou un selon sa parité en retirant 2.
  2. Étape 2 : Lorsque n ≥ 2, n − 2 reste naturel et diminue strictement.
  3. Étape 3 : Le programme doit donc tester zéro et un avant de rappeler pair(n − 2).

Résultat : Les deux bases couvrent toutes les issues. Pour pair(5), les arguments successifs sont 5, 3, 1 ; le résultat est False.

Piège à éviter : Une base limitée à zéro laisse les arguments impairs devenir négatifs sans atteindre la base.

À toi d'essayer

On remplace dans somme l’appel somme(n − 1) par somme(n). Le cas de base existe toujours. Pourquoi cela ne suffit-il plus pour un naturel strictement positif ?

Correction détaillée pas à pasDans ce chapitre : 2 corrections détaillées.

Appliquer la récursivité à un autre domaine

Un palindrome est une chaîne qui se lit de la même façon dans les deux sens. Nous comparons ici les caractères exactement, sans ignorer accents, espaces ou majuscules. La chaîne vide et une chaîne d’une lettre sont des palindromes. Pour une chaîne plus longue, il faut que les deux caractères extrêmes soient égaux et que la chaîne intérieure soit elle-même un palindrome.

La réduction porte cette fois sur la longueur, qui baisse de deux. Le test de longueur doit précéder l’accès au premier ou au dernier caractère : l’indice zéro n’existe pas dans une chaîne vide. L’opérateur and évalue son second opérande seulement si le premier est vrai ; des extrémités différentes suffisent donc à renvoyer False sans explorer le centre.

Python
def palindrome(texte):
  if len(texte) <= 1:
    return True
  return (
    texte[0] == texte[-1]
    and palindrome(texte[1:-1])
  )

Deux lettres égales sont retirées à chaque étape ; une lettre seule est un cas de base.

Exemple résolu

Explique le résultat de palindrome("RADAR") et celui de palindrome("RADARS").

  1. Étape 1 : Pour RADAR, les deux R concordent ; l’appel intérieur porte sur ADA.
  2. Étape 2 : Les deux A concordent ; D est un cas de base vrai, puis les valeurs vraies remontent.
  3. Étape 3 : Pour RADARS, R et S diffèrent dès le premier appel ; la seconde partie de and n’est pas évaluée.

Résultat : RADAR donne True et RADARS donne False. La même règle s’applique à toute chaîne du domaine défini.

Piège à éviter : Ignorer la casse sans l’annoncer modifierait le contrat et changerait le résultat pour certains mots.

À toi d'essayer

Un palindrome de longueur paire peut-il terminer sur une chaîne d’une seule lettre ? Justifie en étudiant uniquement la longueur, sans choisir un mot particulier.

Correction détaillée pas à pasDans ce chapitre : 2 corrections détaillées.

Comparer à une boucle et tester

Pour somme(n), les arguments n, n − 1, …, 0 produisent n + 1 appels. Dans un modèle où une addition coûte une unité, le temps est linéaire en n. Les additions sur de très grands entiers ont un coût propre, laissé de côté dans ce modèle. La profondeur des appels est également linéaire ; Python conserve les contextes des appels en attente dans une pile.

Une boucle peut calculer la même somme avec un accumulateur initialisé à zéro, puis ajouter successivement les entiers de 1 à n inclus. Elle effectue n additions, mais n’accumule pas n contextes de fonction. Elle utilise un nombre constant de variables dans ce modèle. La récursivité n’est donc pas automatiquement plus rapide ; elle exprime surtout une décomposition du problème.

Attention aux sous-problèmes répétés. Dans une définition naïve de Fibonacci, calculer deux appels récursifs peut recalculer plusieurs fois les mêmes valeurs. Le nombre d’appels peut alors croître bien plus vite que la profondeur. Un dessin des appels permet de repérer cette répétition. La programmation dynamique traitera d’autres façons de conserver des résultats déjà obtenus ; elle n’est pas nécessaire pour comprendre la présente leçon.

Pour palindrome, retirer une tranche construit de nouvelles chaînes en Python. La profondeur est proportionnelle à la longueur dans le pire cas, mais on ne doit pas en déduire un temps linéaire sans compter ces copies. Une version utilisant deux indices peut éviter ces tranches ; comparer les algorithmes impose de préciser les opérations réellement effectuées.

Mettre au point sans masquer le problème

  1. Teste les bases : somme(0), palindrome("") et palindrome("X"). Ajoute un cas positif, un cas négatif pour le résultat booléen, et une entrée qui atteint plusieurs niveaux.
  2. Prévois les résultats avant l’exécution. Compare ensuite les résultats observés aux valeurs prévues, pas seulement à l’absence d’erreur.
  3. Pour une trace, affiche temporairement les paramètres à l’entrée et le résultat avant chaque retour, puis retire ces affichages de la fonction finale.
  4. Si la profondeur autorisée est dépassée, examine d’abord la réduction et la taille de l’entrée. Augmenter arbitrairement une limite ne prouve ni arrêt ni correction.

Erreurs fréquentes et bilan

Ne pas écrire return devant un appel peut faire perdre sa valeur : appeler une fonction et renvoyer son résultat sont deux actions différentes. De même, print affiche une information mais ne remplace pas return. Une fonction qui atteint la fin sans return explicite renvoie None, ce qui risque de rendre une addition impossible.

Récursivité et boucle peuvent coexister : ce mélange n’est pas une erreur en soi. Il faut examiner ce que la boucle répète et ce que chaque appel réduit. Enfin, une terminaison mathématique ne promet pas que toute entrée tiendra dans la mémoire et le temps disponibles. La profondeur limitée de Python est une contrainte d’exécution distincte de la validité de la définition.

Vocabulaire

Cas de base :
Situation dont le résultat est obtenu sans nouvel appel récursif.
Variant :
Quantité naturelle strictement décroissante qui sert à prouver la terminaison.
Pile d’appels :
Organisation des contextes des fonctions actives, retirés dans l’ordre inverse des appels.
Précondition :
Hypothèse sur les entrées dans lesquelles le contrat garantit le comportement.

Relier les besoins aux mécanismes

BesoinRéponse
DéroulerDescendre vers les bases, puis remonter les valeurs.
JustifierSéparer terminaison et résultat calculé.
ChoisirComparer la décomposition et le coût des appels ou des boucles.

À retenir

  • Un appel récursif traite une instance du même problème.
  • Une base atteignable et une réduction bien fondée permettent de prouver l’arrêt.
  • Les contextes d’appels conservent les calculs en attente.

Fiche de révision

Pour concevoir une fonction, écris d’abord son domaine et la réponse sur les entrées minimales ; construis ensuite la réduction.

Pour somme(2), les trois appels donnent successivement une base 0, puis les valeurs 1 et 3 à la remontée.

Dans un raisonnement de coût, distingue nombre total d’appels, profondeur maximale et coût des opérations comme les tranches.

Questions fréquentes

Une fonction récursive est-elle toujours plus lente ?

Non. Il faut comparer les opérations, les structures et les implémentations ; la présence d’une récursion ne suffit pas à conclure.

Un cas de base garantit-il l’arrêt ?

Il faut aussi prouver qu’il est atteint depuis toute entrée du domaine et que les autres opérations terminent.

Et maintenant ?

Tu as lu le cours. L’étape suivante tient en une action.

Relis le coursReprends l’essentiel avant de passer à la suite.

Prolonge l’expérience !

Continue à explorer ce chapitre à travers de nouvelles activités, des défis et des expériences interactives. Avec Premium, l’apprentissage continue !

Ici, tu débloques aussi 2 corrections détaillées.

Parent ? Suivez les progrès de votre enfant.

Tous les cours de NSI