Algorithme récursif

méthode de programmation informatique

Un algorithme récursif est un algorithme qui résout un problème en calculant des solutions d'instances plus petites du même problème[1]. L'approche récursive est un des concepts de base en informatique.

Arbre dessiné par un algorithme récursif. On remarque que chaque branche est elle-même une version plus petite d'un arbre.

Les premiers langages de programmation qui ont autorisé l'emploi de la récursivité sont LISP et Algol 60. Depuis, tous les langages de programmation généraux réalisent une implémentation de la récursivité. Pour répéter des opérations, typiquement, un algorithme récursif s'appelle lui-même. On oppose généralement les algorithmes récursifs aux algorithmes itératifs, qui eux, utilisent plutôt des boucles pour et des boucles tant que, pour répéter des opérations.

Un exemple préliminaire : les permutations

modifier
 
Permutations de la phrase « Belle Marquise vos beaux yeux me font mourir d'amour »

Commençons par un exemple tiré du Bourgeois gentilhomme (Acte II Scène IV) de Molière. Le héros, Monsieur Jourdain, veut connaître toutes les manières « galantes » d'écrire un billet. De la phrase Belle Marquise, vos beaux yeux, me font mourir d'amour, il pourrait tirer Vos beaux yeux, belle Marquise, d'amour me font mourir, puis Vos beaux yeux, me font mourir, belle Marquise, d'amour, puis Vos beaux yeux, me font mourir d'amour, belle Marquise et ainsi de suite.

Comment Monsieur Jourdain devrait-il procéder pour engendrer toutes ces permutations ? Le mieux pour lui pour être sûr d'y arriver est d'utiliser un procédé récursif. L'un de ceux-ci est le suivant[2],[3], mais le lecteur peut en imaginer d'autres. Tout d'abord on construit toutes les permutations de la phrase vos beaux yeux -- me font mourir -- d'amour; puis, dans ces permutations, on insère en première position, puis en deuxième position, puis en troisième position, puis en quatrième position le morceau de phrase belle Marquise. L'algorithme est récursif parce qu'il s'invoque lui-même. En effet, pour construire toutes les permutations de belle Marquise -- vos beaux yeux -- me font mourir -- d'amour, il faut construire toutes les permutations de vos beaux yeux -- me font mourir -- d'amour. De plus, l'algorithme est bien un algorithme général, car si Monsieur Jourdain veut améliorer son côté poétique et veut construire, comme le lui dit son maître de philosophie, toutes les permutations de la phrase belle Marquise -- vos beaux yeux -- me font -- mourir -- d'amour, qui a maintenant cinq constituants il procédera de la même façon et encore de la même façon pour la phrase belle Marquise -- vos beaux yeux -- me font -- mourir -- d'amour -- pour vous, qui a six constituants.

Les permutations courtes

modifier

Pour construire les permutations de Belle marquise -- vos beaux yeux -- me font mourir -- d'amour, il faut auparavant construire toutes les permutations de la phrase vos beaux yeux -- me font mourir -- d'amour. Ces permutations sont au nombre de six et sont construites en insérant le morceau de phrase vos beaux yeux en première, deuxième et troisième position dans les deux permutations de la phrase me font mourir -- d'amour (à savoir me font mourir -- d'amour et d'amour -- me font mourir). Comment sont construites ces deux permutations ? En insérant en première position et en deuxième position le segment me font mourir dans les permutations de la phrase d'amour. L'aspect récursif[note 1] de l'algorithme apparaît clairement.

Bien démarrer

modifier

Combien y a-t-il de permutations de la phrase d'amour ? Il n'y en a qu'une seule et c'est le point de départ de l'algorithme récursif. Avoir un cas de base est nécessaire, parce qu'il faut toujours qu'un algorithme récursif ait un point (ou des points) de départ, car sinon, il ne se terminerait pas.

Un exemple liminaire : la factorielle

modifier

Prenons l'exemple de la factorielle. Celle-ci se définit pour des entiers naturels de la fonction suivante :

 

autrement dit :

 

Donc pour définir la fonction qui calcule la factorielle de n, il suffit d'appeler cette même fonction mais en lui demandant de calculer la factorielle de (n-1), et de multiplier le résultat par n. La factorielle de (n-1) sera calculée en calculant la factorielle de (n-2) et ainsi de suite. Il suffit juste de définir que la factorielle de 0 est 1 pour "arrêter la récursion" et ne plus appeler la fonction qui calcule la factorielle.

On voit avec cet exemple deux points majeurs de la récursion :

  • L'algorithme s'appelle lui-même,   a besoin de  
  • Il est nécessaire d'"arrêter la récursion", en définissant un (ou des) cas ("cas de base") où l'algorithme n'a pas besoin de s'appeler lui-même, ceci afin que l'algorithme ne s'invoque pas lui-même indéfiniment.

Présentation plus mathématique

modifier
 

L'idée de la récursivité est d'utiliser une définition équivalente, à savoir une définition par récurrence sur la valeur de l'argument :

 

Autrement dit, la factorielle d'un nombre qui n'est pas 0 est obtenue en multipliant par n la factorielle de n – 1. Cette définition de la factorielle peut se traduire par le programme suivant en pseudo-code :

factorielle(n) =
  si (n = 0) alors 1
  sinon n * factorielle(n-1)

Préciser que factorielle(0) = 1 est fondamental : sans cela la fonction ne serait pas définie et l'algorithme s'invoquerait indéfiniment. Le cas n = 0 est appelé cas de base. Sans sa présence, l'algorithme ne peut pas se terminer. L'autre cas est appelé cas de propagation, c'est lui qui contient l'appel récursif. On peut programmer ainsi d'autres suites telles que la suite de Fibonacci, tandis que la fonction suivante :

syracuse(n) =
  si (n = 0) ou (n = 1) alors 1
  sinon si (n mod 2 = 0) alors syracuse(n/2)
  sinon syracuse(3*n + 1)

définit la fonction identiquement égale à 1 si la conjecture de Syracuse est vraie.

Mais, comme nous l'avons vu dans l'exemple préliminaire, les algorithmes récursifs ne se limitent évidemment pas au calcul de suites récurrentes et de fonctions sur les entiers naturels. Ils permettent de travailler sur des structures de données définies récursivement comme les chaînes de caractères, les listes ou les arbres, ou plus généralement sur des ensembles munis d'une relation bien fondée. Parmi les relations bien fondées les plus connues, il y a l'ordre sur la structure des objets qui donne lieu à la récurrence structurelle et l'ordre naturel sur les entiers positifs qui donne lieu à la récursivité numérique (ou récursivité sur les entiers). Le tri, le problème des tours de Hanoï et la génération des permutations (c'est-à-dire la généralisation de l'exemple de Monsieur Jourdain) sont des exemples paradigmatiques d'application d'algorithmes récursifs.

Un autre exemple : le nombre de partitions d'un entier naturel en au plus q parties

modifier

Nous allons considérer un cas tiré des mathématiques où l'approche récursive s'impose (voir l'article Partition d'un entier) :

Un exemple : Une partie est un naturel positif qui entre dans une somme quand on décompose un nombre en somme de naturels décroissants. Ainsi, les partitions de 5 en au plus 3 parties sont 5, 4+1, 3+2, 3+1+1, 2+2+1. Si on écrit   le nombre de partitions de 5 en au plus 3 parties, on a   et si on écrit   le nombre de partitions de 5 en exactement 3 parties, on a  , ces partitions sont 3+1+1 et 2+2+1.

Les cas aux limites :

  •  , parce que la seule partition de 0 est celle constituée d'aucune partie.
  •   parce que toute partition d'un entier strictement positif a au moins une partie.
  •   pour  , parce que le nombre de parties d'un entier est au plus égal à sa valeur.

Le cas général : On voit facilement que le nombre de partitions de p en au plus q parties est le nombre de partitions de p en exactement q parties plus le nombre de partitions de p en au plus q-1 parties. Donc

 .

Or si p a exactement q parties cela veut dire que toutes ces parties sont strictement positives, on peut donc leur retirer 1. Or si on retire 1 à chacune de ces parties on obtient une partition de p - q en au plus q parties, d'où :

 

et finalement

  •  .

Autrement dit, si  , le nombre de partitions de p en au plus q parties est le nombre de partitions de p-q en au plus q parties plus le nombre de partitions de p en au plus q-1 parties.

On a bien un énoncé récursif.

Retour à l'exemple : on a donc (le lecteur est invité à faire tous les calculs intermédiaires)

 .

Voici la forme complète de la fonction récursive :

d(p, q) = si p = 0 alors 1
         sinon si q = 0 alors 0
         sinon si q > p alors d(p, p)
         sinon d(p-q, q) + d(p, q-1)

D'autres fonctions récursives à plusieurs arguments

modifier

Parmi les fonctions récursives à deux arguments on trouve la fonction d'Ackermann-Péter. Le pgcd peut aussi être présenté récursivement,

pgcd(p, q) = si p = 0 alors q
         sinon si q = 0 alors p
         sinon si q ≤ p alors pgcd(p-q, q)
         sinon pgcd(p, q-p)

de même que les coefficients binomiaux quand ils sont définis par la formule de Pascal.

La fonction de Sudan est une fonction à trois arguments (l'indice n est le troisième argument).

Algorithmes récursifs non numériques

modifier

Nous avons vu dans l'introduction, une présentation informelle d'un algorithme récursif engendrant les permutations, puis nous avons vu des algorithmes récursifs sur les entiers naturels. Nous allons maintenant présenter des algorithmes récursifs sur d'autres structures de données que les entiers naturels.

Algorithmes récursifs sur les listes

modifier

L'opérateur ++ concatène deux listes, autrement dit, prolonge la première liste par la seconde liste.

[ ] ++ ℓ2 = ℓ2
(x:ℓ1) ++ ℓ2 = x:(ℓ1 ++ ℓ2)

Le cas de base est [ ] ++ ℓ2 = ℓ2.

Étant données une fonction et une liste, considérons un algorithme qui produit la liste obtenue en appliquant ladite fonction à tous les éléments de la liste donnée. Traditionnellement cette fonction s'appelle map (en français « applique »). Si la liste donnée est vide, le résultat est la liste vide (qui se note [ ]) ; c'est le cas de base. Si la fonction s'appelle f et si la liste donnée est constituée de x suivi d'une liste alors le résultat est la liste constituée de f x[note 2] suivi de la liste map f ℓ.

map f [ ] = [ ]
map f (x:ℓ) = f x : (map f ℓ)

La fonction ins insère un élément dans une liste à toutes les positions, produisant une liste de listes.

ins x [ ] = x
ins x (y:ℓ) = (x:y:ℓ) : map ((:) y) (ins x ℓ)

La fonction concat prend une liste de listes et concatène ces listes en une seule liste.

concat [ ] = [ ]
concat (ℓ:ℓℓ) = ℓ ++ (concat ℓℓ)

permutations

modifier

permutations est l'algorithme présenté dans le premier exemple. Il prend une liste et retourne la liste de toutes les permutations de cette liste.

permutations [x] = x
permutations x:ℓ = concat (map (ins x) (permutations ℓ))
 
Arbre binaire portant les étiquettes B, R, A, V, O

Algorithmes récursifs sur les arbres binaires

modifier

La taille d'un arbre binaire est le nombre de ses nœuds internes

taille (Feuille x) = 0
taille (t1 • t2) = ( taille t1) + ( taille t2)

motDesFeuilles

modifier

Étant donné un arbre binaire, on peut construire son mot des feuilles. Ainsi le mot des feuilles de l'arbre de la figure ci-contre est BRAVO ou, plus précisément, la liste [`B`,`R`,`A`,`V`,`O`]. L'algorithme récursif motDesFeuilles prend un arbre binaire et retourne une liste.

motDesFeuilles (Feuille x) = [x]
motDesFeuilles (t1 • t2) = ( motDesFeuilles t1) ++ ( motDesFeuilles t2)

Prouver la correction d'un algorithme récursif

modifier

Prouver le bon fonctionnement d'un algorithme récursif nécessite de vérifier deux propriétés : premièrement l'algorithme se termine et deuxièmement si l'algorithme se termine, il fait bien ce qu'on attend de lui (correction partielle). Dans le cas des algorithmes récursifs, ces méthodes sont spécifiques.

Problème de la terminaison

modifier

Pour prouver la terminaison d'un algorithme récursif, la méthode la plus usuelle est la suivante: chacun des ensembles dans lesquels les paramètres prennent leurs valeurs sont équipés d'une relation d'ordre. Cet ordre ne doit avoir que des chaînes descendantes finies (on dit qu'il est bien fondé) et être tel que les invocations internes de l'algorithme se font avec des valeurs plus petites des paramètres, pour l'ordre en question.

Théorème de terminaison

modifier

Soient   un algorithme récursif défini sur un ensemble   et   un ordre bien fondé sur  .

  étant donné, on note,   l'ensemble des   tels que   appelle  .

Soit  , si,  , alors   se termine.

Terminaison de la fonction d qui calcule le nombre de décompositions de p en au plus q sommants

modifier

L'ordre que l'on prend est l'ordre lexicographique sur  . On a

  •   si et seulement si
    •  
    • ou   et  .

Cet ordre est bien fondé.

Terminaison de la fonction Syracuse

modifier

La terminaison d'un algorithme récursif peut être un problème extrêmement difficile. Ainsi personne n'a jusqu'à présent été capable de démontrer que la fonction Syracuse présentée plus haut se termine pour toute valeur de n. Si c'était le cas, elle définirait effectivement la fonction identiquement égale à 1.

Problème de la correction partielle

modifier

Il faut montrer que si les appels internes à l'algorithme font ce qu'on attend d'eux, alors l'algorithme entier fait ce qu'on attend de lui. Dans le cas de Monsieur Jourdain, il faut montrer que si on part d'une suite de toutes les permutations de n-1 éléments, on aboutira à une suite de toutes les permutations de n éléments.

Correction partielle sur l'exemple du pgcd

modifier

Prenons l'exemple du  , il s'agit de montrer que si   et   sont positifs alors

  •  ,

ce qui est la caractérisation du plus grand diviseur commun de deux nombres où   signifie que   divise   (on a, en particulier,   pour tout  ). Notons   cette propriété.

Pour montrer la correction de l'algorithme ci-dessus, on suppose   et on essaie de montrer    est la fonction  .

On procède par cas.
    • Si   alors   et donc  , mais aussi  . De plus, si   et si   alors clairement  , puisque précisément  .
    • Si   on obtient le même résultat.
    • Si  , on a  , d'autre part si on a   alors   divise la somme donc  . D'autre part, si   et   alors par hypothèse   et donc   et donc  , c.q.f.d.
    • Si  , on raisonne comme ci-dessus en inversant les rôles de   et  .

De la démonstration ci-dessus, on déduit que si l'algorithme de   se termine alors il satisfait :

  et donc   calcule bien le plus grand commun diviseur.

Efficacité

modifier

Simplicité de description versus efficacité

modifier

La mise en œuvre des algorithmes récursifs nécessite le plus souvent[note 3] une pile. C'est la difficulté d'implanter cette pile ou d'éviter son emploi qui a fait dire pendant longtemps que les programmes récursifs étaient moins efficaces que les programmes itératifs, mais la situation a changé. En fait, le débat sur le choix entre codage récursif ou itératif est aussi vieux que l'informatique et les progrès de la compilation des langages de programmation réduit encore la différence d'efficacité. Voici quelques arguments en faveur de la présentation récursive :

  • La présentation récursive permet d'expliquer simplement des algorithmes beaucoup plus astucieux et efficaces, comme cela a été montré par Tony Hoare avec son algorithme de tri rapide.
  • Les compilateurs d'aujourd'hui sont tellement astucieux que plus le programme leur est présenté de façon abstraite et sans effets de bord, plus ils peuvent mettre en œuvre leurs optimisations et aboutir à des codes objets efficaces[réf. souhaitée].
  • Des structures de données récursives, comme les quadtrees, ont été conçues pour leur efficacité. La manière la plus naturelle de les gérer est par des algorithmes récursifs.

Contributions

modifier
  • La contribution la plus percutante dans ce débat a été celle de John Backus, l'inventeur du Fortran, qui a pris clairement le parti de la programmation fonctionnelle, donc de la programmation récursive, lors de la remise de son prix Turing en 1977.
  • Niklaus Wirth, l'inventeur du langage de programmation Pascal écrit :

« The power of recursion evidently lies in the possibility of defining an infinite set of objects by a finite statement. In the same manner, an infinite number of computations can be described by a finite recursive program, even if this program contains no explicit repetitions. »

— Niklaus Wirth, Algorithms + Data Structures = Programs[4].

«  La puissance de la récursivité réside évidemment dans la possibilité de définir des ensembles infinis d'objets par une instruction finie. De façon similaire, un nombre infini d'étapes de calcul peut être décrit par un programme récursif fini, même si ce programme ne contient aucune répétition explicite. »

  • C. A. R. Hoare qui a écrit le premier compilateur (du langage Algol 60) implémentant la récursivité, note dans son discours lors de la remise du prix Turing en 1980[5] que son algorithme de tri rapide a été « très difficile à expliquer » tant qu'il ne connaissait pas l'existence des procédures récursives. Il poursuit en parlant de la récursion :

« I have regarded it as the highest goal of programming langage design to enable good ideas to be elegantly expressed. »

— C. A. R. Hoare, The Emperor's Old Clothes[5].

«  J'ai estimé que l'objectif le plus important de la conception d'un langage de programmation était de permettre aux meilleures idées d'être exprimées de manière élégante.  »

Récursion terminale

modifier

Dans la définition d'une fonction récursive  , l'appel récursif est en position terminale si elle est de la forme

 

Dans cette écriture,   et   sont des fonctions indépendantes de  . La fonction   est la condition d'arrêt. L'important, dans cette définition, est que l'appel de   ne soit pas englobé dans une autre expression. Une telle récursion est dite récursion terminale. En pseudo-code, la définition récursive terminale peut s'écrire

fonction f(x)
    si p(x)
        retourner g(x)
    sinon
        retourner f(h(x))

x peut être un n-uplet contenant plusieurs variables.

La définition récursive terminale se transcrit automatiquement en une définition itérative. En pseudo-code, la définition itérative peut s'écrire

fonction f(x)
    tant que vrai
        si p(x)
            retourner g(x)
        sinon
            x ← h(x)

Par exemple, ce programme Python donne une définition récursive non terminale fact de la factorielle :

def fact(n):
    if n == 0:
        return 1
    else:
        return n * fact(n - 1)

En effet, n * fact(n - 1) englobe l'appel à fact. Mais on peut la transformer en une définition récursive terminale par l'ajout d'un argument a appelé accumulateur[6].

Ce programme Python donne une définition récursive terminale fact_iter de la factorielle :

def fact_iter(n, a):
    if n == 0:
        return a
    else:
        return fact_iter(n - 1, n * a)

def fact(n):
    return fact_iter(n, 1)

tandis que ce programme Python donne une définition itérative fact_iter de la factorielle :

def fact_iter(n, a):
    while True:
        if n == 0:
            return a
        else:
            n, a = n - 1, n * a

def fact(n):
    return fact_iter(n, 1)

Autres situations et corécursivité

modifier

La récursivité (et sa duale la corécursivité (en)) se retrouvent dans d'autres situations, où elle prend parfois d'autres noms.

 
Tapis de Sierpiński.
  • L'autosimilarité est le caractère d'un objet dans laquelle on peut trouver des similarités en l'observant à différentes échelles. C'est de la corécursivité.
  • Les fractales ont cette propriété d'autosimilarité, mais elles ont plutôt à voir avec le concept un peu différent qui s'appelle la corécursivité (en) (ou corécursion). Le tapis de Sierpiński, du nom de Wacław Sierpiński, est une fractale obtenue à partir d'un carré. Le tapis se fabrique en découpant le carré en neuf carrés égaux avec une grille de trois par trois, et en supprimant la pièce centrale, et en appliquant cette procédure récursivement aux huit carrés restants.
 
Les Époux Arnolfini par Jan van Eyck, National Gallery, Londres.
Dans le miroir suspendu sur le mur, on voit le couple de dos et le peintre lui-même qui se reflètent à l’envers.
  • La mise en abyme est un procédé consistant à représenter une œuvre dans une œuvre similaire, par exemple en incrustant dans une image cette image elle-même. On retrouve dans ce principe l'« autosimilarité » et le principe des fractales ou de la corécursivité en mathématiques. Des publicités emploient ce procédé, dont la fameuse Vache qui rit de Benjamin Rabier.
  • Quand on écrit
fibs :: [Integer]
fibs = 0:1:zipWith (+) (fibs) (tail fibs)
en Haskell, il s'agit de corécursivité. On définit ainsi une suite infinie (un stream) de toutes les valeurs de la suite de Fibonacci. On pourra récupérer la n-ième valeur de fibs, que l'on ne calcule qu'une fois, et obtenir ainsi le n-ième nombre de Fibonacci. Cette suite infinie est décrite en langue naturelle ainsi, c'est la suite fibs qui commence par 0, suivi de 1, puis des éléments obtenus en ajoutant le premier élément de la suite fibs au deuxième de la suite fibs pour former le troisième élément de la suite fibs, puis le deuxième de la suite fibs au troisième de la suite fibs pour former le quatrième élément de la suite fibs, puis ainsi de suite, le i-ème élément de la suite fibs au i+1-ème de la suite fibs pour former le i+2-ème élément de la suite fibs, etc.

Notes et références

modifier

Références

modifier
  1. Ronald L. Graham, Donald E. Knuth et Oren Patashnik (trad. Alain Denise), Mathématiques concrètes : fondations pour l'informatique, Vuibert, coll. « Vuibert informatique », , 2e éd., 687 p. (ISBN 978-2-7117-4824-2), p. 1.
  2. Donald E. Knuth, The Art of Computer Programming (Generating All Tuples and Permutations), vol. 4.2, Addison Wesley, p. 41
  3. Jan Christiansen, Nikita Danilenko et Sandra Dylus, « All sorts of permutations (functional pearl) », Proceedings of the 21st ACM-SIGPLAN International Conference on Functional Programming, (ICFP),‎ , p. 168--179 (lire en ligne)
  4. (en) Niklaus Wirth, Algorithms + Data Structures = Programs, Englewood Cliffs, New Jersey, Prentice-Hall, Inc., , 366 p. (ISBN 978-0-13-022418-7), p. 129.
  5. a et b (en) Charles Antony Richard Hoare, « The Emperor's Old Clothes », Communications of the ACM, vol. 24, no 2,‎ , p. 75-83 (lire en ligne).
  6. Harold Abelson, Gerald Jay Sussman (auteurs) et Julie Sussman (collaboratrice), Structure and interpretation of computer programs, MIT Press et McGraw-Hill, , 2e éd., XXIII+657 (ISBN 978-0-07-000484-9, lire en ligne), chap. 1.2.1 (« Linear Recursion and Iteration »).
  1. L'algorithme s'invoque lui-même de façon répétitive
  2. On remarquera que l'on note l'application d'une fonction f x ce qui est traditionnel dans les langages de programmation fondés sur la récursivité.
  3. Les fonctions récursives terminales par exemple ne requièrent pas de pile.

Voir aussi

modifier

Articles connexes

modifier

Sur les autres projets Wikimedia :

Bibliographie

modifier

De très nombreux livres et articles traitant des algorithmes récursifs ont été publiés. Nous n'en donnons ici qu'une liste non exhaustive.

En français

modifier
  • Anne Brygoo, Titou Durand, Maryse Pelletier, Christian Queinnec et al. Programmation récursive (en Scheme) Cours et exercices corrigés Collection: Sciences Sup, Dunod, 2004
  • Emmanuel Chailloux, Pascal Manoury et Bruno Pagano, Développement d'applications avec Objective Caml, Éditions O'Reilly, Paris, 2000
  • Sylvain Conchon et Jean-Christophe Filliâtre, Apprendre à programmer avec OCaml. Algorithmes et structures de données. Éditions Eyrolles, 2014
  • C. Livercy, Theorie des programmes / schemas, preuves, sémantique, Dunod, 1978
  • Niklaus Wirth, Algorithmes et structures de données, 1987, trad. par J.A. Hernandez et P. Kruchten / 2e édition / Paris : Eyrolles , rééd. 1989
  • Laurent Arditi et Stéphane Ducasse, La programmation : une approche fonctionnelle et récursive avec Scheme

En anglais

modifier