Ich habe in meiner langjährigen Programmierung viel Rekursion verwendet, um einfache Probleme zu lösen, aber ich bin mir völlig bewusst, dass Sie manchmal aufgrund von Speicher- / Geschwindigkeitsproblemen eine Iteration benötigen.
Irgendwann in der Vergangenheit habe ich versucht herauszufinden, ob es ein "Muster" oder eine Lehrbuchmethode gibt, um einen gemeinsamen Rekursionsansatz in Iteration umzuwandeln, und nichts gefunden. Oder zumindest nichts, woran ich mich erinnern kann, würde helfen.
- Gibt es allgemeine Regeln?
- Gibt es ein "Muster"?
recursion
computer-science
theory
iteration
Gustavo Carreno
quelle
quelle
Antworten:
Normalerweise ersetze ich einen rekursiven Algorithmus durch einen iterativen Algorithmus, indem ich die Parameter, die normalerweise an die rekursive Funktion übergeben werden, auf einen Stapel schiebe. Tatsächlich ersetzen Sie den Programmstapel durch einen eigenen.
Hinweis: Wenn Sie mehr als einen rekursiven Aufruf enthalten und die Reihenfolge der Aufrufe beibehalten möchten, müssen Sie diese in umgekehrter Reihenfolge zum Stapel hinzufügen:
muss ersetzt werden durch
Bearbeiten: Der Artikel Stacks and Recursion Elimination (oder Article Backup Link ) enthält weitere Details zu diesem Thema.
quelle
(node)->()
durch "(node)->[actions]
wo Aktion ist" ersetzt habe() -> [actions]
. Dann nehmen Sie außen einfach eine Aktion / Fortsetzung vom Stapel, wenden sie an / führen sie aus, verschieben die auf dem Stapel zurückgegebenen Aktionen in umgekehrter Reihenfolge und wiederholen sie. Kontingente / komplexe Durchläufe, Sie erfassen nur lokale Stapelvariablen in Zeigern mit Referenzzählung, die Sie in Ihren Thunks schließen, und nachfolgende Thunks können von den Ergebnissen früherer Sub-Traversals usw.new
, können wir anstelle des Stapels ein Objekt auf dem Heap erstellen. Im Gegensatz zum Stapel hat der Heap keine Speicherbeschränkungen. Siehe gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.htmlWirklich ist der häufigste Weg, dies zu tun, Ihren eigenen Stapel zu behalten. Hier ist eine rekursive Quicksort-Funktion in C:
So könnten wir es iterativ machen, indem wir unseren eigenen Stapel behalten:
Offensichtlich werden in diesem Beispiel die Stapelgrenzen nicht überprüft ... und Sie können den Stapel tatsächlich anhand des ungünstigsten Falls für die linken und rechten Werte dimensionieren. Aber du kommst auf die Idee.
quelle
O(N) = O(R*L)
, wobeiL
die Summe der Komplexität "für Schicht r" ist. In diesem Fall müssen SieO(N)
bei jedem Schritt die Partitionierungen durchführen. Die rekursive Tiefe ist hierO(R)
, dh im schlimmsten FallO(N)
, der durchschnittliche FallO(logN)
.Es scheint, dass niemand angesprochen hat, wo sich die rekursive Funktion mehr als einmal im Körper aufruft und die Rückkehr zu einem bestimmten Punkt in der Rekursion (dh nicht primitiv-rekursiv) behandelt. Es wird gesagt, dass jede Rekursion in Iteration umgewandelt werden kann , so dass dies möglich sein sollte.
Ich habe mir gerade ein C # -Beispiel ausgedacht, wie das geht. Angenommen, Sie haben die folgende rekursive Funktion, die sich wie eine Nachbestellungsdurchquerung verhält, und AbcTreeNode ist ein 3-ariger Baum mit den Zeigern a, b, c.
Die iterative Lösung:
quelle
Bemühen Sie sich, Ihren rekursiven Aufruf Tail Recursion (Rekursion, bei der die letzte Anweisung der rekursive Aufruf ist) durchzuführen. Sobald Sie das haben, ist die Konvertierung in Iteration im Allgemeinen ziemlich einfach.
quelle
Im Allgemeinen kann die Rekursion als Iteration nachgeahmt werden, indem einfach eine Speichervariable verwendet wird. Beachten Sie, dass Rekursion und Iteration im Allgemeinen gleichwertig sind. man kann fast immer zum anderen konvertieren. Eine rekursive Schwanzfunktion lässt sich sehr leicht in eine iterative umwandeln. Machen Sie einfach die Akkumulatorvariable zu einer lokalen und iterieren Sie anstatt zu rekursieren. Hier ist ein Beispiel in C ++ (C wäre es nicht für die Verwendung eines Standardarguments):
Als ich mich kannte, habe ich wahrscheinlich einen Fehler im Code gemacht, aber die Idee ist da.
quelle
Selbst die Verwendung eines Stapels konvertiert einen rekursiven Algorithmus nicht in einen iterativen. Normale Rekursion ist eine funktionsbasierte Rekursion. Wenn wir einen Stapel verwenden, wird sie zu einer stapelbasierten Rekursion. Aber es ist immer noch Rekursion.
Für rekursive Algorithmen ist die Raumkomplexität O (N) und die Zeitkomplexität O (N). Bei iterativen Algorithmen beträgt die Raumkomplexität O (1) und die Zeitkomplexität O (N).
Wenn wir jedoch Stapel verwenden, bleibt die Komplexität gleich. Ich denke, nur die Schwanzrekursion kann in Iteration umgewandelt werden.
quelle
copy = new int[size]; for(int i=0; i<size; ++i) copy[i] = source[i];
und die zeitliche Komplexität sind beide O (N), basierend auf der Größe der Daten, aber es ist eindeutig ein iterativer Algorithmus.Der Artikel zur Beseitigung von Stapeln und Rekursionen enthält die Idee, den Stapelrahmen auf dem Heap zu externalisieren, bietet jedoch keine einfache und wiederholbare Möglichkeit zum Konvertieren. Unten ist einer.
Bei der Konvertierung in iterativen Code muss beachtet werden, dass der rekursive Aufruf von einem beliebig tiefen Codeblock aus erfolgen kann. Es geht nicht nur um die Parameter, sondern auch um den Punkt, an dem die noch auszuführende Logik und der Status der Variablen, die an nachfolgenden Bedingungen beteiligt sind, von Bedeutung sind. Im Folgenden finden Sie eine sehr einfache Möglichkeit, mit geringsten Änderungen in iterativen Code zu konvertieren.
Betrachten Sie diesen rekursiven Code:
Iterativer Code:
Beachten Sie, dass die Struktur des Codes weiterhin der rekursiven Logik entspricht und die Änderungen minimal sind, was zu einer geringeren Anzahl von Fehlern führt. Zum Vergleich habe ich die Änderungen mit ++ und - markiert. Die meisten der neu eingefügten Blöcke mit Ausnahme von v.push_back sind allen konvertierten iterativen Logik gemeinsam
+++++++++++++++++++++++++
------------------------
+++++++++++++++++++++++++
-------------------------
+++++++++++++++++++++++++
-------------------------
+++++++++++++++++++++++++
-------------------------
quelle
stackitem
Objekte werden mit einem Müllwert für zugewiesenra
. Alles funktioniert immer noch im ähnlichsten Fall, aber sollte esra
zufällig 1 oder 2 sein, erhalten Sie ein falsches Verhalten. Die Lösung besteht darin,ra
auf 0 zu initialisieren .stackitem
darf nicht ohne Initialisierung gepusht werden. Aber ja, das Initialisieren auf 0 würde Fehler auffangen.v.pop_back()
stattdessen beide Absenderadressen auf die Anweisung festgelegt?Suchen Sie in Google nach "Continuation Passing Style". Es gibt ein allgemeines Verfahren zum Konvertieren in einen rekursiven Endstil. Es gibt auch ein allgemeines Verfahren zum Umwandeln von rekursiven Schwanzfunktionen in Schleifen.
quelle
Nur die Zeit totzuschlagen ... Eine rekursive Funktion
kann konvertiert werden zu
quelle
Im Allgemeinen wird die Technik zur Vermeidung eines Stapelüberlaufs für rekursive Funktionen als Trampolintechnik bezeichnet, die von Java-Entwicklern weit verbreitet ist.
Doch für C # gibt es ein kleines Hilfsmethode hier die Ihre rekursive Funktion zu iterative schaltet ohne Änderung Logik zu erfordern oder den Code in-verständlich machen. C # ist eine so schöne Sprache, dass damit erstaunliche Dinge möglich sind.
Es funktioniert, indem Teile der Methode mit einer Hilfsmethode umbrochen werden. Zum Beispiel die folgende rekursive Funktion:
Verwandelt sich in:
quelle
Denken Sie an Dinge, die tatsächlich einen Stapel benötigen:
Wenn wir das Rekursionsmuster betrachten als:
Zum Beispiel der klassische Turm von Hanoi
Dies kann in eine Schleife übersetzt werden, die an einem expliziten Stapel arbeitet, indem es wie folgt angepasst wird:
Für den Turm von Hanoi wird dies:
Hier besteht eine erhebliche Flexibilität bei der Definition Ihres Stapels. Sie können Ihren Stapel zu einer Liste von
Command
Objekten machen, die anspruchsvolle Dinge tun. Oder Sie können in die entgegengesetzte Richtung gehen und eine Liste mit einfacheren Typen erstellen (z. B. kann eine "Aufgabe" 4 Elemente auf einem Stapel vonint
statt eines Elements auf einem Stapel von seinTask
).Dies bedeutet lediglich, dass sich der Speicher für den Stapel im Heap und nicht im Java-Ausführungsstapel befindet. Dies kann jedoch hilfreich sein, da Sie mehr Kontrolle darüber haben.
quelle
Ein zu suchendes Muster ist ein Rekursionsaufruf am Ende der Funktion (sogenannte Schwanzrekursion). Dies kann leicht durch eine Weile ersetzt werden. Zum Beispiel die Funktion foo:
endet mit einem Anruf bei foo. Dies kann ersetzt werden durch:
Dadurch wird der zweite rekursive Aufruf eliminiert.
quelle
Eine Frage , die als Duplikat dieser Frage geschlossen worden war, hatte eine sehr spezifische Datenstruktur:
Der Knoten hatte die folgende Struktur:
Die rekursive Löschfunktion sah folgendermaßen aus:
Im Allgemeinen ist es nicht immer möglich, einen Stapel für rekursive Funktionen zu vermeiden, die sich mehr als einmal (oder sogar einmal) aufrufen. Für diese spezielle Struktur ist es jedoch möglich. Die Idee ist, alle Knoten in einer einzigen Liste zusammenzufassen. Dies wird erreicht, indem die aktuellen Knoten
child
am Ende der Liste der obersten Zeile platziert werden.Diese Technik kann auf jede datengebundene Struktur angewendet werden, die auf eine DAG mit einer deterministischen topologischen Reihenfolge reduziert werden kann. Die aktuellen untergeordneten Knoten werden neu angeordnet, sodass das letzte untergeordnete Element alle anderen untergeordneten Elemente übernimmt. Dann kann der aktuelle Knoten gelöscht werden und das Durchlaufen kann dann zum verbleibenden untergeordneten Knoten iterieren.
quelle
Rekursion ist nichts anderes als das Aufrufen einer Funktion von der anderen. Nur dieser Vorgang erfolgt durch Aufrufen einer Funktion für sich. Wie wir wissen, speichert die erste Funktion beim Aufrufen der anderen Funktion ihren Status (ihre Variablen) und übergibt die Steuerung dann an die aufgerufene Funktion. Die aufgerufene Funktion kann unter Verwendung des gleichen Namens von Variablen aufgerufen werden. Ex fun1 (a) kann fun2 (a) aufrufen. Wenn wir rekursiv aufrufen, passiert nichts Neues. Eine Funktion ruft sich selbst auf, indem sie denselben Typ und ähnliche Namensvariablen (aber offensichtlich sind die in Variablen gespeicherten Werte unterschiedlich, nur der Name bleibt gleich) an sich selbst übergibt. Vor jedem Aufruf speichert die Funktion ihren Status und dieser Speichervorgang wird fortgesetzt. Die Einsparung erfolgt auf einem Stapel.
JETZT KOMMT DER STAPEL ZUM SPIELEN.
Wenn Sie also jedes Mal ein iteratives Programm schreiben und den Status auf einem Stapel speichern und dann bei Bedarf die Werte aus dem Stapel entfernen, haben Sie ein rekursives Programm erfolgreich in ein iteratives Programm konvertiert!
Der Beweis ist einfach und analytisch.
Bei der Rekursion verwaltet der Computer einen Stapel, und bei der iterativen Version müssen Sie den Stapel manuell verwalten.
Denken Sie darüber nach, konvertieren Sie einfach ein rekursives Programm für die Tiefensuche (in Diagrammen) in ein iteratives dfs-Programm.
Alles Gute!
quelle
Ein weiteres einfaches und vollständiges Beispiel für die Umwandlung der rekursiven Funktion in eine iterative Funktion mithilfe des Stapels.
quelle
Eine grobe Beschreibung, wie ein System eine rekursive Funktion übernimmt und sie mithilfe eines Stapels ausführt:
Dies sollte die Idee ohne Details zeigen. Betrachten Sie diese Funktion, mit der Knoten eines Diagramms ausgedruckt werden:
Zum Beispiel Grafik: A-> B A-> C show (A) würde B, A, C drucken
Funktionsaufrufe bedeuten, den lokalen Status und den Fortsetzungspunkt zu speichern, damit Sie zurückkehren und dann die Funktion überspringen können, die Sie aufrufen möchten.
Angenommen, Show (A) beginnt zu laufen. Der Funktionsaufruf in Zeile 3. show (B) bedeutet - Element zum Stapel hinzufügen, was bedeutet, dass Sie in Zeile 2 mit dem lokalen Variablenstatus node = A fortfahren müssen. - Gehen Sie zu Zeile 0 mit node = B.
Um Code auszuführen, durchläuft das System die Anweisungen. Wenn ein Funktionsaufruf auftritt, sendet das System Informationen, die es benötigt, um dorthin zurückzukehren, wo es war, führt den Funktionscode aus und zeigt nach Abschluss der Funktion die Informationen darüber an, wohin es gehen muss, um fortzufahren.
quelle
Dieser Link bietet einige Erklärungen und schlägt die Idee vor, "Ort" beizubehalten, um zwischen mehreren rekursiven Aufrufen an den genauen Ort zu gelangen:
Alle diese Beispiele beschreiben jedoch Szenarien, in denen ein rekursiver Aufruf eine feste Anzahl von Malen ausgeführt wird. Die Dinge werden schwieriger, wenn Sie etwas haben wie:
quelle
Es gibt eine allgemeine Möglichkeit, die rekursive Durchquerung in einen Iterator umzuwandeln, indem ein verzögerter Iterator verwendet wird, der mehrere Iteratorlieferanten verkettet (Lambda-Ausdruck, der einen Iterator zurückgibt). Siehe meine Konvertierung der rekursiven Durchquerung in einen Iterator .
quelle
Meine Beispiele sind in Clojure, sollten aber ziemlich einfach in jede Sprache zu übersetzen sein.
Bei dieser Funktion gilt
StackOverflow
s für große Werte von n:Wir können eine Version definieren, die ihren eigenen Stapel auf folgende Weise verwendet:
wo
return
ist definiert als:Dies funktioniert auch für komplexere Funktionen, zum Beispiel die ackermann-Funktion :
kann umgewandelt werden in:
quelle