Chaque récursion peut-elle être convertie en itération?


181

Un fil de discussion reddit a soulevé une question apparemment intéressante:

Les fonctions récursives de queue peuvent être converties de manière simple en fonctions itératives. D'autres peuvent être transformés en utilisant une pile explicite. Peut chaque récursion se transformer en itération?

L'exemple (compteur?) Dans l'article est la paire:

(define (num-ways x y)
  (case ((= x 0) 1)
        ((= y 0) 1)
        (num-ways2 x y) ))

(define (num-ways2 x y)
  (+ (num-ways (- x 1) y)
     (num-ways x (- y 1))

3
Je ne vois pas en quoi c'est un contre-exemple. La technique de pile fonctionnera. Ce ne sera pas joli et je ne vais pas l'écrire, mais c'est faisable. Il semble akdas reconnaît que dans votre lien.
Matthew Flaschen

Votre (nombre-manières xy) est juste (x + y) choosex = (x + y)! / (X! Y!), Qui n'a pas besoin de récursivité.
ShreevatsaR


Je dirais que la récursivité n'est qu'une commodité.
e2-e4

Réponses:


181

Pouvez-vous toujours transformer une fonction récursive en une fonction itérative? Oui, absolument, et la thèse de Church-Turing le prouve si la mémoire est bonne. En termes simples, il déclare que ce qui est calculable par des fonctions récursives est calculable par un modèle itératif (tel que la machine de Turing) et vice versa. La thèse ne vous dit pas précisément comment faire la conversion, mais elle dit que c'est certainement possible.

Dans de nombreux cas, la conversion d'une fonction récursive est facile. Knuth propose plusieurs techniques dans "L'art de la programmation informatique". Et souvent, une chose calculée récursivement peut être calculée par une approche complètement différente en moins de temps et d'espace. L'exemple classique de ceci est les nombres de Fibonacci ou leurs séquences. Vous avez sûrement rencontré ce problème dans votre plan d'études.

De l'autre côté de cette médaille, on peut certainement imaginer un système de programmation suffisamment avancé pour traiter une définition récursive d'une formule comme une invitation à mémoriser des résultats antérieurs, offrant ainsi l'avantage de la vitesse sans avoir à dire à l'ordinateur exactement quelles étapes à suivre suivre dans le calcul d'une formule avec une définition récursive. Dijkstra a presque certainement imaginé un tel système. Il a passé un long moment à essayer de séparer l'implémentation de la sémantique d'un langage de programmation. Là encore, ses langages de programmation non déterministes et multiprocesseurs sont dans une ligue au-dessus du programmeur professionnel en exercice.

En dernière analyse, de nombreuses fonctions sont tout simplement plus faciles à comprendre, à lire et à écrire sous forme récursive. À moins qu'il n'y ait une raison impérieuse, vous ne devriez probablement pas (manuellement) convertir ces fonctions en un algorithme explicitement itératif. Votre ordinateur gérera ce travail correctement.

Je peux voir une raison convaincante. Supposons que vous ayez un système prototype dans un langage de très haut niveau comme [ enfiler des sous-vêtements en amiante ] Scheme, Lisp, Haskell, OCaml, Perl ou Pascal. Supposons que les conditions soient telles que vous ayez besoin d'une implémentation en C ou Java. (C'est peut-être de la politique.) Alors vous pourriez certainement avoir des fonctions écrites récursivement mais qui, traduites littéralement, feraient exploser votre système d'exécution. Par exemple, la récursivité infinie de la queue est possible dans Scheme, mais le même idiome pose un problème pour les environnements C existants. Un autre exemple est l'utilisation de fonctions imbriquées lexicalement et de portée statique, que Pascal prend en charge mais pas C.

Dans ces circonstances, vous pourriez essayer de surmonter la résistance politique à la langue d'origine. Vous pourriez vous retrouver à réimplémenter mal Lisp, comme dans la dixième loi de Greenspun (ironique). Ou vous pourriez simplement trouver une approche complètement différente de la solution. Mais en tout cas, il y a sûrement un moyen.


10
Church-Turing n'est-il pas encore à prouver?
Liran Orevi

15
@eyelidlessness: Si vous pouvez implémenter A dans B, cela signifie que B a au moins autant de puissance que A. Si vous ne pouvez pas exécuter une instruction de A dans l'implémentation A-de-B, alors ce n'est pas une implémentation. Si A peut être implémenté en B et B peut être implémenté en A, puissance (A)> = puissance (B) et puissance (B)> = puissance (A). La seule solution est puissance (A) == puissance (B).
Tordek

6
re: 1er paragraphe: Vous parlez d'équivalence de modèles de calcul, pas de la thèse de Church-Turing. L'équivalence était AFAIR prouvée par Church et / ou Turing, mais ce n'est pas la thèse. La thèse est un fait expérimental que tout ce qui est intuitivement calculable est calculable au sens mathématique strict (par des machines de Turing / des fonctions récursives, etc.). Il pourrait être réfuté si, en utilisant les lois de la physique, nous pouvions construire des ordinateurs non classiques calculant quelque chose que les machines de Turing ne peuvent pas faire (par exemple, un problème d'arrêt). Alors que l'équivalence est un théorème mathématique, et elle ne sera pas réfutée.
sdcvvc

7
Comment diable cette réponse a-t-elle obtenu des votes positifs? D'abord, il mélange l'exhaustivité de Turing avec la thèse de Church-Turing, puis il fait un tas de handwaving incorrect, mentionnant des systèmes "avancés" et abandonnant la récursivité infinie paresseuse de la queue (ce que vous pouvez faire en C ou dans n'importe quel langage complet de Turing parce que ... euh. . est-ce que quelqu'un sait ce que signifie Turing complet?). Ensuite, une conclusion pleine d'espoir, comme celle-ci était une question sur Oprah et tout ce dont vous avez besoin est d'être positif et édifiant? Réponse horrible!
ex0du5

8
Et les bs sur la sémantique ??? Vraiment? C'est une question sur les transformations syntaxiques, et c'est devenu en quelque sorte un excellent moyen de nommer drop Dijkstra et de laisser entendre que vous savez quelque chose sur le pi-calcul? Permettez-moi de clarifier ceci: que l'on regarde la sémantique dénotationnelle d'un langage ou d'un autre modèle n'aura aucune incidence sur la réponse à cette question. Que le langage soit l'assemblage ou un langage de modélisation de domaine génératif ne signifie rien. Il ne s'agit que de l'exhaustivité de Turing et de la transformation de "variables de pile" en "pile de variables".
ex0du5

43

Est-il toujours possible d'écrire une forme non récursive pour chaque fonction récursive?

Oui. Une preuve formelle simple est de montrer que la récursion µ et un calcul non récursif tel que GOTO sont tous deux complets de Turing. Puisque tous les calculs complets de Turing sont strictement équivalents dans leur pouvoir expressif, toutes les fonctions récursives peuvent être implémentées par le calcul non récursif de Turing-complet.

Malheureusement, je ne parviens pas à trouver une bonne définition formelle de GOTO en ligne, alors en voici une:

Un programme GOTO est une séquence de commandes P exécutées sur une machine de registre telle que P est l'une des suivantes:

  • HALT, qui arrête l'exécution
  • r = r + 1rest un registre
  • r = r – 1rest un registre
  • GOTO xxest une étiquette
  • IF r ≠ 0 GOTO xrest un registre et xest une étiquette
  • Une étiquette, suivie de l'une des commandes ci-dessus.

Cependant, les conversions entre les fonctions récursives et non récursives ne sont pas toujours triviales (sauf par une ré-implémentation manuelle insensée de la pile d'appels).

Pour plus d'informations, consultez cette réponse .


Très bonne réponse! Cependant, dans la pratique, j'ai beaucoup de mal à transformer les algos récursifs en algos itératifs. Par exemple, je n'ai pas pu jusqu'à présent transformer le typeur monomorphe présenté ici community.topcoder.com/ ... en un algorithme itératif
Nils

31

La récursivité est implémentée sous forme de piles ou de constructions similaires dans les interpréteurs ou compilateurs réels. Vous pouvez donc certainement convertir une fonction récursive en une contrepartie itérative, car c'est toujours comme cela que cela se fait (si automatiquement) . Vous allez simplement dupliquer le travail du compilateur de manière ad hoc et probablement d'une manière très moche et inefficace.


13

Fondamentalement, oui, ce que vous finissez par avoir à faire est de remplacer les appels de méthode (qui poussent implicitement l'état sur la pile) dans des poussées de pile explicites pour se souvenir où `` l'appel précédent '' était arrivé, puis d'exécuter la `` méthode appelée '' au lieu.

J'imagine que la combinaison d'une boucle, d'une pile et d'une machine à états pourrait être utilisée pour tous les scénarios en simulant essentiellement les appels de méthode. Il n'est pas vraiment possible de dire si cela va être «meilleur» (soit plus rapide, soit plus efficace dans un certain sens) en général.


9
  • Le flux d'exécution de fonction récursive peut être représenté sous forme d'arborescence.

  • La même logique peut être réalisée par une boucle, qui utilise une structure de données pour traverser cet arbre.

  • La traversée en profondeur peut être effectuée à l'aide d'une pile, la traversée en largeur peut être effectuée à l'aide d'une file d'attente.

Donc, la réponse est oui. Pourquoi: https://stackoverflow.com/a/531721/2128327 .

Une récursion peut-elle être effectuée en une seule boucle? Oui parce que

une machine de Turing fait tout ce qu'elle fait en exécutant une seule boucle:

  1. chercher une instruction,
  2. l'évaluer,
  3. aller à 1.

7

Oui, en utilisant explicitement une pile (mais la récursivité est beaucoup plus agréable à lire, à mon humble avis).


17
Je ne dirais pas que c'est toujours plus agréable à lire. L'itération et la récursivité ont leur place.
Matthew Flaschen

6

Oui, il est toujours possible d'écrire une version non récursive. La solution triviale consiste à utiliser une structure de données de pile et à simuler l'exécution récursive.


Ce qui va à l'encontre de l'objectif si votre structure de données de pile est allouée sur la pile, ou prend beaucoup plus de temps si elle est allouée sur le tas, non? Cela me semble trivial mais inefficace.
conradkleinespel

1
@conradk Dans certains cas, c'est la chose pratique à faire si vous devez effectuer une opération d'arborescence récursive sur un problème suffisamment grand pour épuiser la pile d'appels; la mémoire du tas est généralement beaucoup plus abondante.
jamesdlin

4

En principe, il est toujours possible de supprimer la récursivité et de la remplacer par une itération dans un langage qui a un état infini à la fois pour les structures de données et pour la pile d'appels. C'est une conséquence fondamentale de la thèse de Church-Turing.

Étant donné un langage de programmation réel, la réponse n'est pas aussi évidente. Le problème est qu'il est tout à fait possible d'avoir un langage où la quantité de mémoire pouvant être allouée dans le programme est limitée mais où la quantité de pile d'appels pouvant être utilisée est illimitée (C 32 bits où l'adresse des variables de pile n'est pas accessible). Dans ce cas, la récursivité est plus puissante simplement parce qu'elle a plus de mémoire qu'elle peut utiliser; il n'y a pas assez de mémoire explicitement allouable pour émuler la pile d'appels. Pour une discussion détaillée à ce sujet, consultez cette discussion .


2

Toutes les fonctions calculables peuvent être calculées par les machines de Turing et donc les systèmes récursifs et les machines de Turing (systèmes itératifs) sont équivalents.


1

Parfois, le remplacement de la récursivité est beaucoup plus facile que cela. La récursivité était la chose à la mode enseignée dans CS dans les années 1990, et donc beaucoup de développeurs moyens de cette époque pensaient que si vous résolviez quelque chose avec la récursivité, c'était une meilleure solution. Donc, ils utiliseraient la récursivité au lieu de faire une boucle en arrière pour inverser l'ordre, ou des choses idiotes comme ça. Donc, parfois, la suppression de la récursivité est un simple exercice de type «duh, c'était évident».

C'est moins un problème maintenant, car la mode s'est déplacée vers d'autres technologies.



0

Outre la pile explicite, un autre modèle pour convertir la récursivité en itération est l'utilisation d'un trampoline.

Ici, les fonctions renvoient soit le résultat final, soit une fermeture de l'appel de fonction qu'elles auraient autrement effectué. Ensuite, la fonction d'initiation (trampoline) continue d'appeler les fermetures retournées jusqu'à ce que le résultat final soit atteint.

Cette approche fonctionne pour les fonctions mutuellement récursives, mais je crains que cela ne fonctionne que pour les appels de fin.

http://en.wikipedia.org/wiki/Trampoline_(computers)


0

Je dirais que oui - un appel de fonction n'est rien d'autre qu'un goto et une opération de pile (en gros). Tout ce que vous avez à faire est d'imiter la pile qui est construite en invoquant des fonctions et de faire quelque chose de similaire à goto (vous pouvez imiter gotos avec des langages qui n'ont pas explicitement ce mot-clé).


1
Je pense que l'OP recherche une preuve ou autre chose de substantiel
Tim



-1

tazzego, la récursivité signifie qu'une fonction s'appellera elle-même que vous le vouliez ou non. Quand les gens parlent de savoir si les choses peuvent ou non être faites sans récursivité, ils le pensent et vous ne pouvez pas dire "non, ce n'est pas vrai, parce que je ne suis pas d'accord avec la définition de la récursivité" comme une déclaration valide.

Dans cet esprit, à peu près tout ce que vous dites est absurde. La seule autre chose que vous dites qui n'est pas absurde est l'idée que vous ne pouvez pas imaginer la programmation sans pile d'appels. C'est quelque chose qui avait été fait pendant des décennies jusqu'à ce que l'utilisation d'une pile d'appels devienne populaire. Les anciennes versions de FORTRAN n'avaient pas de pile d'appels et elles fonctionnaient très bien.

À propos, il existe des langages complets de Turing qui n'implémentent que la récursivité (par exemple SML) comme moyen de bouclage. Il existe également des langages complets de Turing qui n'implémentent l'itération que comme moyen de bouclage (par exemple, FORTRAN IV). La thèse de Church-Turing prouve que tout ce qui est possible dans un langage uniquement récursif peut être fait dans un langage non récursif et vica-versa par le fait qu'ils ont tous deux la propriété de turing-complétude.


-3

Voici un algorithme itératif:

def howmany(x,y)
  a = {}
  for n in (0..x+y)
    for m in (0..n)
      a[[m,n-m]] = if m==0 or n-m==0 then 1 else a[[m-1,n-m]] + a[[m,n-m-1]] end
    end
  end
  return a[[x,y]]
end
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.