47 votes

Tous les algorithmes itératifs peuvent-ils être exprimés de manière récursive ?

Sinon, existe-t-il un bon contre-exemple montrant un algorithme itératif pour lequel il n'existe pas de contrepartie récursive ?

S'il est vrai que tous les algorithmes itératifs peuvent être exprimés de manière récursive, existe-t-il des cas où cela est plus difficile à faire ?

Par ailleurs, quel rôle le langage de programmation joue-t-il dans tout cela ? Je peux imaginer que les programmeurs Scheme ont une vision différente de l'itération (= récursion en queue) et de l'utilisation de la pile que les programmeurs Java.

1 votes

79voto

plinth Points 26817

Il existe une preuve ad hoc simple pour cela. Puisque vous pouvez construire un langage complet de Turing en utilisant des structures strictement itératives et un langage complet de Turing en utilisant uniquement des structures récursives, les deux sont donc équivalents.

6 votes

Wow, ça donne à réfléchir. J'envie tous les votants qui ont digéré cela plus rapidement que moi. Il est temps de se documenter sur ce sujet.

0 votes

Attends, ce n'est pas une erreur logique ? Un raisonnement circulaire ?

8 votes

C Bauer : En fait, ce n'est pas le cas. La preuve est très facile à faire : supposez des langages IT (avec des constructions itératives seulement) et REC (avec des constructions récursives seulement). Simulez une machine de turing universelle en utilisant IT, puis simulez une machine de turing universelle en utilisant REC. L'existence des programmes simulateurs garantit que IT et REC peuvent calculer toutes les fonctions calculables. Cette propriété est prouvée pour le lambda calculus, où toutes les fonctions sont partiellement récursives.

19voto

Norman Ramsey Points 115730

Tous les algorithmes itératifs peuvent-ils être exprimés de manière récursive ?

Oui, mais la preuve n'est pas intéressante :

  1. Transformez le programme avec tout son flux de contrôle en une seule boucle contenant une seule déclaration de cas dans laquelle chaque branche est un flux de contrôle en ligne droite comprenant éventuellement break , return , exit , raise et ainsi de suite. Introduisez une nouvelle variable (appelée "compteur de programme") que l'instruction case utilise pour décider du bloc à exécuter ensuite.

    Cette construction a été découverte pendant les grandes "guerres de programmation structurée" des années 1960, lorsque les gens discutaient du pouvoir expressif relatif de diverses constructions de flux de contrôle.

  2. Remplacez la boucle par une fonction récursive, et remplacez chaque variable locale mutable par un paramètre de cette fonction. Voilà ! L'itération est remplacée par la récursion.

Cette procédure revient à écrire un interprète pour la fonction d'origine. Comme vous pouvez l'imaginer, cela donne un code illisible, et ce n'est pas une chose intéressante à faire. Cependant Dans le cas de l'apprentissage de la programmation fonctionnelle, certaines des techniques peuvent être utiles à une personne ayant une expérience de la programmation impérative et qui apprend à programmer dans un langage fonctionnel pour la première fois.

1 votes

Aw, @Norman, c'est une chose intéressante à faire pour un compilateur. En fait la procédure est : transformer le code impératif en code fonctionnel, puis, transformer le code fonctionnel en code impératif. Pourquoi est-ce intéressant ? Parce que le code fonctionnel a une sémantique simple mais ne peut pas être exécuté, alors que le code impératif est incompréhensible mais peut être exécuté. En particulier, le code fonctionnel est facile à optimiser pour les choses de haut niveau et le code impératif pour les choses de bas niveau (mais le mélange initial est difficile à utiliser dans n'importe quel but).

0 votes

Les choses vraiment inintéressantes sont la sémantique de ces primitives de contrôle à simuler par des appels de fonctions de récursion. Bien que parfois elles puissent rendre le intention derrière le code source plus clair, cela rend souvent difficile de raisonner sur les choses qu'ils font réellement en plus de simuler l'exécution dans les contextes originaux. Par contre, les appels de fonction ont des méthodes bien connues pour être réduits rapidement pour les utilisateurs expérimentés. Les différences consistent à savoir si vous avez la permission d'utiliser certaines équations (plutôt que de tout faire arithmétiquement) pour résoudre le problème.

8voto

Chris Jester-Young Points 102876

Comme vous le dites, toute approche itérative peut être transformée en approche "récursive", et avec des appels de queue, la pile n'explosera pas non plus :-) En fait, c'est ainsi que Scheme implémente toutes les formes courantes de boucles. Exemple en Scheme :

(define (fib n)
  (do ((x 0 y)
       (y 1 (+ x y))
       (i 1 (+ i 1)))
      ((> i n) x)))

Ici, bien que la fonction semble itérative, elle récure en fait un lambda interne qui prend trois paramètres, x , y y i et s'appelle lui-même avec de nouvelles valeurs à chaque itération.

Voici une façon dont cette fonction pourrait être macro-étendue :

(define (fib n)
  (letrec ((inner (lambda (x y i)
                    (if (> i n) x
                        (inner y (+ x y) (+ i 1))))))
    (inner 0 1 1)))

De cette façon, la nature récursive devient plus apparente visuellement.

4 votes

Et notez bien que cualquier L'algorithme itératif peut être transformé en un algorithme récursif de queue. Par exemple, il suffit de le transformer en un algorithme de type continuation-passing.

0 votes

J'ajouterais simplement que le compilateur de chaque langage n'optimise pas les appels de queue, de sorte que la pile peut effectivement "exploser" (déborder) dans les langages utilisant la récursion de queue (ex. C#).

7voto

Johan Points 1599

Définir itératif comme :

function q(vars):
  while X:
    do Y

Peut être traduit par :

 function q(vars):
    if X:
      do Y
      call q(vars)

Dans la plupart des cas, Y inclut l'incrémentation d'un compteur testé par X. Cette variable devra être transmise dans 'vars' d'une manière ou d'une autre si l'on utilise la voie récursive.

1voto

rjzii Points 8979

Comme l'a noté Plinth dans le leur réponse nous pouvons construire des preuves montrant comment récursion et l'itération sont équivalentes et peuvent toutes deux être utilisées pour résoudre le même problème. Cependant, même si nous savons que les deux sont équivalentes, il existe des inconvénients à utiliser l'une plutôt que l'autre.

Dans les langages qui ne sont pas optimisés pour la récursion, vous pouvez constater qu'un algorithme utilisant l'itération s'exécute plus rapidement que le récursif. De même, même dans les langages optimisés, vous pouvez constater qu'un algorithme utilisant l'itération écrit dans un langage différent s'exécute plus rapidement que le récursif. En outre, il se peut qu'il n'y ait pas une manière évidente d'écrire un algorithme donné en utilisant la récursion par rapport à l'itération et vice versa. Cela peut conduire à un code difficile à lire, ce qui entraîne des problèmes de maintenabilité.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X