Kann jede Rekursion in Iteration umgewandelt werden?


180

Ein reddit-Thread warf eine anscheinend interessante Frage auf:

Schwanzrekursive Funktionen können trivial in iterative Funktionen umgewandelt werden. Andere können mithilfe eines expliziten Stapels transformiert werden. Kann jede Rekursion in Iteration umgewandelt werden?

Das (Zähler?) Beispiel im Beitrag ist das Paar:

(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
Ich sehe nicht, wie dies ein Gegenbeispiel ist. Die Stapeltechnik wird funktionieren. Es wird nicht schön sein und ich werde es nicht schreiben, aber es ist machbar. Es scheint, dass akdas dies in Ihrem Link anerkennt.
Matthew Flaschen

Ihre (Anzahl xy) ist nur (x + y) choosex = (x + y)! / (X! Y!), Die keine Rekursion benötigt.
ShreevatsaR


Ich würde sagen, dass Rekursion nur eine Annehmlichkeit ist.
e2-e4

Antworten:


180

Können Sie eine rekursive Funktion immer in eine iterative umwandeln? Ja, absolut, und die Church-Turing-These beweist es, wenn das Gedächtnis dient. In Laien ausgedrückt heißt es, dass das, was durch rekursive Funktionen berechenbar ist, durch ein iteratives Modell (wie die Turing-Maschine) berechenbar ist und umgekehrt. Die These sagt Ihnen nicht genau, wie Sie die Konvertierung durchführen sollen, aber sie besagt, dass dies definitiv möglich ist.

In vielen Fällen ist das Konvertieren einer rekursiven Funktion einfach. Knuth bietet verschiedene Techniken in "The Art of Computer Programming" an. Und oft kann eine rekursiv berechnete Sache durch einen völlig anderen Ansatz in weniger Zeit und Raum berechnet werden. Das klassische Beispiel hierfür sind Fibonacci-Zahlen oder Sequenzen davon. Sie haben dieses Problem sicherlich in Ihrem Studienplan getroffen.

Auf der anderen Seite dieser Medaille können wir uns sicherlich ein Programmiersystem vorstellen, das so fortschrittlich ist, dass es eine rekursive Definition einer Formel als Aufforderung behandelt, sich frühere Ergebnisse zu merken, und so den Geschwindigkeitsvorteil bietet, ohne dem Computer genau sagen zu müssen, zu welchen Schritten er vorgehen soll Folgen Sie der Berechnung einer Formel mit einer rekursiven Definition. Dijkstra hat sich mit ziemlicher Sicherheit ein solches System vorgestellt. Er hat lange versucht, die Implementierung von der Semantik einer Programmiersprache zu trennen. Andererseits sind seine nicht deterministischen und mehrverarbeitenden Programmiersprachen in einer Liga über dem praktizierenden professionellen Programmierer.

Letztendlich sind viele Funktionen in rekursiver Form einfach einfacher zu verstehen, zu lesen und zu schreiben. Sofern es keinen zwingenden Grund gibt, sollten Sie diese Funktionen wahrscheinlich nicht (manuell) in einen explizit iterativen Algorithmus konvertieren. Ihr Computer wird diesen Job korrekt ausführen.

Ich kann einen zwingenden Grund sehen. Angenommen, Sie haben ein Prototypsystem in einer Sprache auf höchstem Niveau wie [ Asbestunterwäsche anziehen ], Lisp, Haskell, OCaml, Perl oder Pascal. Angenommen, die Bedingungen sind so, dass Sie eine Implementierung in C oder Java benötigen. (Vielleicht ist es Politik.) Dann könnten Sie sicherlich einige Funktionen rekursiv schreiben lassen, die aber wörtlich übersetzt Ihr Laufzeitsystem explodieren lassen würden. Beispielsweise ist in Schema eine unendliche Schwanzrekursion möglich, aber dieselbe Redewendung verursacht ein Problem für vorhandene C-Umgebungen. Ein weiteres Beispiel ist die Verwendung von lexikalisch verschachtelten Funktionen und statischen Bereichen, die Pascal unterstützt, C jedoch nicht.

Unter diesen Umständen könnten Sie versuchen, den politischen Widerstand gegen die Originalsprache zu überwinden. Sie könnten feststellen, dass Sie Lisp schlecht umsetzen, wie im zehnten Gesetz von Greenspun (ironisch). Oder Sie finden einfach einen völlig anderen Lösungsansatz. Aber auf jeden Fall gibt es einen Weg.


10
Ist Church-Turing nicht noch zu beweisen?
Liran Orevi

15
@eyelidlessness: Wenn Sie A in B implementieren können, bedeutet dies, dass B mindestens so viel Leistung wie A hat. Wenn Sie eine Anweisung von A in der A-Implementierung von B nicht ausführen können, handelt es sich nicht um eine Implementierung. Wenn A in B implementiert werden kann und B in A implementiert werden kann, ist Leistung (A)> = Leistung (B) und Leistung (B)> = Leistung (A). Die einzige Lösung ist Leistung (A) == Leistung (B).
Tordek

6
Betreff: 1. Absatz: Sie sprechen von der Gleichwertigkeit von Rechenmodellen, nicht von der Church-Turing-These. Die Gleichwertigkeit wurde AFAIR von Church und / oder Turing bewiesen, aber es ist nicht die These. Die These ist eine experimentelle Tatsache, dass alles, was intuitiv berechenbar ist, im streng mathematischen Sinne berechenbar ist (durch Turing-Maschinen / rekursive Funktionen usw.). Es könnte widerlegt werden, wenn wir unter Verwendung der Gesetze der Physik einige nichtklassische Computer bauen könnten, die etwas berechnen, was Turing-Maschinen nicht können (z. B. das Anhalten eines Problems). Während die Äquivalenz ein mathematischer Satz ist und nicht widerlegt wird.
SDCVVC

7
Wie zum Teufel hat diese Antwort positive Stimmen bekommen? Zuerst verwechselt es die Vollständigkeit von Turing mit der These von Church-Turing, dann macht es eine Menge falscher Handbewegungen, erwähnt "fortgeschrittene" Systeme und lässt die faule unendliche Schwanzrekursion fallen (was Sie in C oder einer beliebigen vollständigen Turing-Sprache tun können, weil ... ähm. Weiß jemand, was Turing komplett bedeutet?). Dann war ein hoffnungsvolles Hand-Holding-Fazit wie dieses eine Frage zu Oprah und alles, was Sie brauchen, ist, positiv und erhebend zu sein? Schreckliche Antwort!
ex0du5

8
Und das bs über Semantik ??? "Ja wirklich?" Dies ist eine Frage zu syntaktischen Transformationen, und irgendwie ist es eine großartige Möglichkeit geworden, drop Dijkstra zu benennen und zu implizieren, dass Sie etwas über den Pi-Kalkül wissen? Lassen Sie mich dies klarstellen: Ob man die Denotationssemantik einer Sprache oder eines anderen Modells betrachtet, hat keinen Einfluss auf die Antwort auf diese Frage. Ob die Sprache Assembler oder eine generative Domänenmodellierungssprache ist, bedeutet nichts. Es geht nur darum, die Vollständigkeit zu überprüfen und "Stapelvariablen" in "einen Variablenstapel" umzuwandeln.
ex0du5

43

Ist es immer möglich, für jede rekursive Funktion ein nicht rekursives Formular zu schreiben?

Ja. Ein einfacher formaler Beweis besteht darin, zu zeigen, dass sowohl die µ-Rekursion als auch ein nicht rekursiver Kalkül wie GOTO vollständig sind. Da alle vollständigen Turing-Kalküle in ihrer Ausdruckskraft streng gleichwertig sind, können alle rekursiven Funktionen durch den nicht rekursiven Turing-vollständigen Kalkül implementiert werden.

Leider kann ich online keine gute, formale Definition von GOTO finden. Hier ist eine:

Ein GOTO-Programm ist eine Folge von Befehlen P, die auf einer Registermaschine ausgeführt werden, so dass P einer der folgenden ist:

  • HALT, was die Ausführung anhält
  • r = r + 1Wo rist ein Register?
  • r = r – 1Wo rist ein Register?
  • GOTO xWo xist ein Etikett?
  • IF r ≠ 0 GOTO xWo rist ein Register und xist ein Etikett
  • Eine Bezeichnung, gefolgt von einem der oben genannten Befehle.

Die Konvertierung zwischen rekursiven und nicht rekursiven Funktionen ist jedoch nicht immer trivial (außer durch sinnlose manuelle Neuimplementierung des Aufrufstapels).

Weitere Informationen finden Sie in dieser Antwort .


Gute Antwort! In der Praxis habe ich jedoch große Schwierigkeiten, rekursive Algen in iterative Algen umzuwandeln. Zum Beispiel war ich bisher nicht in der Lage, den hier vorgestellten monomorphen Typer community.topcoder.com/… in einen iterativen Algorithmus umzuwandeln
Nils

31

Die Rekursion wird als Stapel oder ähnliche Konstrukte in den tatsächlichen Interpreten oder Compilern implementiert. Sie können also eine rekursive Funktion in ein iteratives Gegenstück konvertieren, da dies immer (wenn auch automatisch) erfolgt . Sie werden die Arbeit des Compilers nur ad-hoc und wahrscheinlich auf sehr hässliche und ineffiziente Weise duplizieren.


13

Grundsätzlich ja, im Wesentlichen müssen Sie am Ende Methodenaufrufe (die den Status implizit auf den Stapel übertragen) durch explizite Stapel-Pushs ersetzen, um sich daran zu erinnern, wo der 'vorherige Aufruf' angekommen war, und dann die 'aufgerufene Methode' ausführen. stattdessen.

Ich würde mir vorstellen, dass die Kombination einer Schleife, eines Stapels und einer Zustandsmaschine für alle Szenarien verwendet werden kann, indem die Methodenaufrufe im Grunde simuliert werden. Ob dies "besser" sein wird oder nicht (entweder schneller oder in gewissem Sinne effizienter), kann man im Allgemeinen nicht wirklich sagen.


9
  • Der Ausführungsfluss rekursiver Funktionen kann als Baum dargestellt werden.

  • Dieselbe Logik kann von einer Schleife ausgeführt werden, die eine Datenstruktur verwendet, um diesen Baum zu durchlaufen.

  • Die Tiefendurchquerung kann mithilfe eines Stapels erfolgen, die Breitendurchquerung kann mithilfe einer Warteschlange erfolgen.

Die Antwort lautet also: Ja. Warum: https://stackoverflow.com/a/531721/2128327 .

Kann eine Rekursion in einer einzigen Schleife durchgeführt werden? Ja, weil

Eine Turing-Maschine erledigt alles, was sie tut, indem sie eine einzelne Schleife ausführt:

  1. eine Anweisung holen,
  2. bewerten Sie es,
  3. gehe zu 1.

6

Ja, es ist immer möglich, eine nicht rekursive Version zu schreiben. Die triviale Lösung besteht darin, eine Stapeldatenstruktur zu verwenden und die rekursive Ausführung zu simulieren.


Was macht entweder den Zweck zunichte, wenn Ihre Stapeldatenstruktur auf dem Stapel zugewiesen ist, oder dauert viel länger, wenn sie auf dem Heap zugewiesen ist, nein? Das klingt für mich trivial, aber ineffizient.
Conradkleinespel

1
@conradk In einigen Fällen ist es praktisch, wenn Sie eine baumrekursive Operation für ein Problem ausführen müssen, das groß genug ist, um den Aufrufstapel zu erschöpfen. Heap-Speicher ist in der Regel viel umfangreicher.
Jamesdlin

6

Ja, explizit einen Stapel verwenden (aber Rekursion ist meiner Meinung nach viel angenehmer zu lesen).


16
Ich würde nicht sagen, dass es immer angenehmer ist zu lesen. Sowohl Iteration als auch Rekursion haben ihren Platz.
Matthew Flaschen

3

Im Prinzip ist es immer möglich, die Rekursion zu entfernen und durch Iteration in einer Sprache zu ersetzen, die sowohl für Datenstrukturen als auch für den Aufrufstapel einen unendlichen Zustand hat. Dies ist eine grundlegende Konsequenz der Church-Turing-These.

Bei einer tatsächlichen Programmiersprache ist die Antwort nicht so offensichtlich. Das Problem ist, dass es durchaus möglich ist, eine Sprache zu haben, in der die Menge des im Programm zuweisbaren Speichers begrenzt ist, die Menge des verwendbaren Aufrufstapels jedoch unbegrenzt ist (32-Bit-C, wobei die Adresse der Stapelvariablen angegeben ist) ist nicht zugänglich). In diesem Fall ist die Rekursion einfach deshalb leistungsfähiger, weil sie über mehr Speicher verfügt, den sie verwenden kann. Es ist nicht genügend explizit zuweisbarer Speicher vorhanden, um den Aufrufstapel zu emulieren. Eine ausführliche Diskussion hierzu finden Sie in dieser Diskussion .


2

Alle berechenbaren Funktionen können von Turing-Maschinen berechnet werden, und daher sind die rekursiven Systeme und Turing-Maschinen (iterative Systeme) äquivalent.


1

Manchmal ist das Ersetzen der Rekursion viel einfacher. Rekursion war in den 90er Jahren die Mode, die in CS gelehrt wurde, und so dachten viele durchschnittliche Entwickler aus dieser Zeit, wenn Sie etwas mit Rekursion lösten, war es eine bessere Lösung. Also würden sie Rekursion verwenden, anstatt sich rückwärts zu drehen, um die Reihenfolge umzukehren, oder dumme Dinge wie diese. Manchmal ist das Entfernen von Rekursion eine einfache Art von Übung, die "duh, das war offensichtlich".

Dies ist jetzt weniger problematisch, da sich die Mode in Richtung anderer Technologien verlagert hat.



0

Abgesehen vom expliziten Stapel ist die Verwendung eines Trampolins ein weiteres Muster für die Umwandlung von Rekursion in Iteration.

Hier geben die Funktionen entweder das Endergebnis zurück oder schließen den Funktionsaufruf, den er sonst ausgeführt hätte. Dann ruft die Initiierungsfunktion (Trampolin) die zurückgegebenen Verschlüsse so lange auf, bis das Endergebnis erreicht ist.

Dieser Ansatz funktioniert für gegenseitig rekursive Funktionen, aber ich fürchte, er funktioniert nur für Tail-Calls.

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


0

Ich würde ja sagen - ein Funktionsaufruf ist nichts anderes als eine Goto- und eine Stack-Operation (grob gesagt). Alles, was Sie tun müssen, ist, den Stapel zu imitieren, der beim Aufrufen von Funktionen erstellt wurde, und etwas Ähnliches wie goto zu tun (Sie können gotos mit Sprachen imitieren, die dieses Schlüsselwort nicht explizit haben).


1
Ich denke, das OP sucht nach einem Beweis oder etwas anderem Wesentlichem
Tim

0

Schauen Sie sich die folgenden Einträge auf Wikipedia an. Sie können sie als Ausgangspunkt verwenden, um eine vollständige Antwort auf Ihre Frage zu finden.

Folgt einem Absatz, der Ihnen einen Hinweis geben kann, wo Sie anfangen sollen:

Das Lösen einer Wiederholungsrelation bedeutet, eine Lösung in geschlossener Form zu erhalten : eine nicht rekursive Funktion von n.

Schauen Sie sich auch den letzten Absatz dieses Eintrags an .



-1

Tazzego, Rekursion bedeutet, dass sich eine Funktion selbst aufruft, ob Sie es mögen oder nicht. Wenn Leute darüber sprechen, ob Dinge ohne Rekursion gemacht werden können oder nicht, meinen sie dies und Sie können nicht "Nein, das ist nicht wahr, weil ich mit der Definition von Rekursion nicht einverstanden bin" als gültige Aussage sagen.

In diesem Sinne ist fast alles, was Sie sagen, Unsinn. Das einzige andere, was Sie sagen, das ist kein Unsinn, ist die Idee, dass Sie sich das Programmieren ohne Callstack nicht vorstellen können. Dies wurde jahrzehntelang getan, bis die Verwendung eines Callstacks populär wurde. In alten Versionen von FORTRAN fehlte ein Callstack, und sie funktionierten einwandfrei.

Übrigens gibt es Turing-vollständige Sprachen, die nur Rekursion (z. B. SML) als Mittel zum Schleifen implementieren. Es gibt auch Turing-vollständige Sprachen, die Iteration nur als Mittel zum Schleifen implementieren (z. B. FORTRAN IV). Die Church-Turing-These beweist, dass alles, was in rekursiven Sprachen möglich ist, in einer nicht rekursiven Sprache und umgekehrt durchgeführt werden kann, indem beide die Eigenschaft der Turing-Vollständigkeit haben.


-3

Hier ist ein iterativer Algorithmus:

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
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.