edubun
Français

La récursivité

Déplacer une tour de Hanoï de 64 disques demande 18 446 744 073 709 551 615 déplacements ; une fonction récursive décrit la solution en quatre lignes.

Une fonction qui s'appelle elle-même

Certains problèmes contiennent une copie plus petite d'eux-mêmes. La factorielle de 5, notée 5!, vaut 5 × 4 × 3 × 2 × 1 = 120. Regarde-la à nouveau : c'est 5 × (4 × 3 × 2 × 1), c'est-à-dire 5 × 4!. En général, n! = n × (n − 1)! : pour calculer une factorielle, on peut donc calculer une factorielle plus petite et multiplier. Une fonction qui résout un problème en s'appelant elle-même sur une version plus petite est récursive.

function factorial(n) {
  if (n <= 1) return 1;          // cas de base : on s'arrête ici
  return n * factorial(n - 1);   // cas récursif : un problème plus petit
}
console.log(factorial(5)); // affiche 120

Toute fonction récursive a besoin de deux parties. Le cas de base est une entrée assez petite pour qu'on y réponde directement, sans autre appel : ici, la factorielle de 1 vaut 1. Le cas récursif réduit le problème et rappelle la fonction. Chaque appel doit se rapprocher du cas de base. Sinon, par exemple si tu écrivais factorial(n + 1) par erreur, les appels ne s'arrêteraient jamais.

La pile d'appels

Comment l'ordinateur garde-t-il la trace de tous ces appels inachevés ? Quand factorial(5) s'exécute, il arrive à 5 * factorial(4) et doit attendre la réponse. L'ordinateur place l'appel en attente sur une pile, un tas où le dernier élément posé est le premier retiré. factorial(4) attend factorial(3), et ainsi de suite jusqu'à factorial(1), qui renvoie 1 tout de suite. Puis la pile se dépile : factorial(2) renvoie 2, factorial(3) renvoie 6, factorial(4) renvoie 24 et factorial(5) renvoie 120.

La pile a une taille limitée. Une récursion qui n'atteint jamais son cas de base continue d'ajouter des appels jusqu'à épuisement de la place, et JavaScript s'arrête avec une erreur ; dans Chrome et Node.js, le message est « RangeError: Maximum call stack size exceeded ». À cause de cette même limite, une récursion très profonde, sur des centaines de milliers de niveaux, s'écrit plutôt avec une boucle.

Les tours de Hanoï

Les tours de Hanoï sont un casse-tête publié par le mathématicien français Édouard Lucas en 1883. Il y a trois piquets et une pile de disques de tailles différentes sur le premier piquet, le plus grand en bas. Le but est de déplacer toute la pile sur le troisième piquet. On ne peut déplacer qu'un disque à la fois, seulement le disque du dessus d'un piquet, et on ne doit jamais poser un disque sur un plus petit.

Réfléchir étape par étape à l'endroit où va chaque disque devient vite confus. Réfléchir de façon récursive rend la solution courte. Pour déplacer n disques de A vers C : d'abord, on écarte les n − 1 disques du dessus, de A vers B. Puis on déplace le plus grand disque de A vers C. Puis on déplace les n − 1 disques de B pour les poser dessus, en C. Déplacer n − 1 disques est le même casse-tête, une taille en dessous, et déplacer un seul disque est le cas de base. Pour 3 disques, cela donne 7 déplacements ; le labo montre la pile des appels en attente à chaque déplacement.

function hanoi(n, from, to, via) {
  if (n === 0) return;
  hanoi(n - 1, from, via, to);
  console.log("disk " + n + ": " + from + " -> " + to);
  hanoi(n - 1, via, to, from);
}
hanoi(3, "A", "C", "B");  // 7 déplacements
Chargement

Fais glisser pour tourner. Fais défiler ou pince pour zoomer.

Trois disques, sept déplacements. La pile d'appels, à droite, liste les appels hanoi qui attendent la fin de ce déplacement. Essaie avec 4 et 5 disques.

Combien de déplacements demandent n disques ? Déplacer n disques coûte deux déplacements de n − 1 disques plus un : M(n) = 2M(n − 1) + 1, avec M(1) = 1. Cela donne 1, 3, 7, 15, 31, donc M(n) = 2ⁿ − 1, et aucune solution ne peut en utiliser moins. Dix disques demandent 1023 déplacements. Soixante-quatre disques en demandent 2⁶⁴ − 1 = 18 446 744 073 709 551 615 ; à un déplacement par seconde, cela fait environ 585 milliards d'années. C'est la croissance en O(2ⁿ) de l'unité précédente.

Récursivité, boucles et travail répété

Tout ce qui s'écrit avec la récursivité peut s'écrire avec une boucle et une pile gérée soi-même, et les cas simples comme la factorielle sont souvent plus clairs avec une boucle. La récursivité brille quand le problème a lui-même la forme d'un arbre : couper une liste dans le tri fusion, explorer des dossiers dans des dossiers, ou chercher un chemin dans un labyrinthe, le sujet de la dernière unité. Un piège est le travail répété. Les nombres de Fibonacci 0, 1, 1, 2, 3, 5, 8, … suivent fib(n) = fib(n − 1) + fib(n − 2). Écrit directement en récursif, fib(10) = 55 fait 177 appels, fib(20) en fait 21 891 et fib(30) en fait 2 692 537, car les mêmes petites valeurs sont recalculées encore et encore. Retenir les réponses déjà trouvées, ou construire à partir du bas avec une boucle, supprime ce gaspillage.

récursivitéUne fonction qui résout un problème en s'appelant elle-même sur une version plus petite
cas de baseUne entrée à laquelle on répond directement, ce qui arrête les appels
cas récursifLa partie qui réduit le problème et rappelle la fonction
pile d'appelsLa pile des appels qui attendent une réponse, dernier entré, premier sorti
débordement de pileTrop d'appels en attente : RangeError en JavaScript
factorielle n!n × (n − 1) × … × 1 ; 5! = 120
Tours de HanoïLucas, 1883 : 2ⁿ − 1 déplacements pour n disques
  1. Combien vaut 6! (factorielle de 6) ?
  2. Combien de déplacements demandent les tours de Hanoï avec 4 disques ?
  3. Combien de déplacements avec 10 disques ?
  4. Que fait le cas de base ?
    • Il répond directement à une petite entrée et arrête la chaîne d'appels
    • Il accélère la fonction
    • Il appelle la fonction avec une entrée plus grande
    • Il affiche le résultat
  5. Que se passe-t-il si une fonction récursive n'atteint jamais son cas de base ?
    • Elle renvoie 0
    • La pile d'appels se remplit et le programme s'arrête avec une erreur
    • Elle tourne en silence pour toujours, sans erreur
    • JavaScript choisit un cas de base à ta place
  6. Dans quel ordre une pile retire-t-elle les éléments ?
    • Dernier entré, premier sorti
    • Premier entré, premier sorti
    • Le plus petit d'abord
    • Au hasard
  7. Pendant que factorial(5) calcule factorial(1), combien d'appels factorial sont sur la pile, celui-ci compris ?
  8. Pour déplacer n disques de A vers C, quelle est la première étape du plan récursif ?
    • Déplacer les n − 1 disques du dessus de A vers B
    • Déplacer le plus grand disque vers C
    • Déplacer le plus petit disque vers C
    • Déplacer tous les disques vers B
  9. Qui a publié le casse-tête des tours de Hanoï en 1883 ?
    • Leonhard Euler
    • Édouard Lucas
    • Tony Hoare
    • Euclide
  10. La version récursive simple de fib fait 177 appels pour fib(10). Combien vaut fib(10) ?
  11. Pourquoi la version récursive simple de fib est-elle lente ?
    • Elle calcule les mêmes petites valeurs de nombreuses fois
    • La récursivité est toujours lente
    • Les additions coûtent cher
    • Elle n'a pas de cas de base
L'essentiel
  • Une fonction récursive s'appelle elle-même sur une version plus petite du problème.
  • Elle a besoin d'un cas de base qui arrête les appels.
  • Les appels en attente se trouvent sur la pile d'appels, dernier entré, premier sorti.
  • Tours de Hanoï : 2ⁿ − 1 déplacements ; 64 disques demanderaient environ 585 milliards d'années à un déplacement par seconde.
  • Méfie-toi du travail répété, comme dans la version récursive simple de Fibonacci.
Sources de ce cours (11)
  • MIT OpenCourseWare 6.006 Introduction to Algorithms, Spring 2020 (Demaine, Ku, Solomon)
  • lecture notes 1 Introduction, 3 Sorting, 9 Breadth-First Search, 10 Depth-First Search (ocw.mit.edu/courses/6-006-introduction-to-algorithms-spring-2020/pages/lecture-notes) and Fall 2011 lecture 13 BFS notes (ocw.mit.edu/courses/6-006-introduction-to-algorithms-fall-2011)
  • Cormen, Leiserson, Rivest, Stein, Introduction to Algorithms (MIT Press)
  • with the CLRS-based Khan Academy algorithms unit by Thomas Cormen and Devin Balkcom of Dartmouth (khanacademy.org/computing/computer-science/algorithms: binary search, selection sort analysis, merge sort, quicksort overview and analysis)
  • MacTutor History of Mathematics, Al-Khwarizmi (mathshistory.st-andrews.ac.uk/Biographies/Al-Khwarizmi: word algorithm from the Latin title Algoritmi de numero Indorum)
  • D. E. Joyce, Euclid's Elements Book VII Propositions 1 and 2 (mathcs.clarku.edu/~djoyce/elements/bookVII)
  • Wolfram MathWorld, Merge Sort (mathworld.wolfram.com/MergeSort.html: proposed by John von Neumann in 1945, after Knuth 1998 p. 159)
  • Stanford CS "Tony Hoare: Quicksort" (cs.stanford.edu/people/eroberts/courses/soco/projects/2008-09/tony-hoare/quicksort.html: devised while a student at Moscow State University for Russian-English machine translation with a tape dictionary, programmed in Algol 60 and published 1961; the year 1959 from the Wikipedia Quicksort article and its cited sources)
  • Bodleian Libraries, Ada Lovelace (blogs.bodleian.ox.ac.uk/adalovelace: 1843 paper, Note G Bernoulli numbers)
  • Fourmilab text of Menabrea's Sketch with Lovelace's notes (fourmilab.ch/babbage/sketch.html)
  • Seven Bridges of Konigsberg (Euler 1736) and Tower of Hanoi (Edouard Lucas, 1883) checked against the Wikipedia articles and their cited sources.

Consulté le: 2026-10-17