Es gibt Zeiten, in denen die Verwendung von Rekursion besser ist als die Verwendung einer Schleife, und Zeiten, in denen die Verwendung einer Schleife besser ist als die Verwendung von Rekursion. Wenn Sie das "richtige" auswählen, können Sie Ressourcen sparen und / oder weniger Codezeilen erhalten.
Gibt es Fälle, in denen eine Aufgabe nur durch Rekursion und nicht durch eine Schleife ausgeführt werden kann?
INC (r)
,JZDEC (r, z)
kann eine Turing - Maschine implementieren. Es gibt keine 'Rekursion' - das ist ein Sprung, wenn nichts anderes als DECrement. Wenn die Ackermann-Funktion berechenbar ist (ist), kann diese Registriermaschine dies tun.Antworten:
Ja und nein. Letztendlich kann keine Rekursion berechnet werden, die eine Schleife nicht kann, aber eine Schleife erfordert viel mehr Installationsaufwand. Daher ist die einzige Sache, die eine Rekursion tun kann, dass Schleifen manche Aufgaben nicht so einfach machen können.
Geh auf einen Baum. Mit Rekursion auf einen Baum zu gehen ist einfach dumm. Es ist das Natürlichste auf der Welt. Ein Baum mit Schleifen zu gehen ist viel weniger einfach. Sie müssen einen Stapel oder eine andere Datenstruktur verwalten, um zu verfolgen, was Sie getan haben.
Oft ist die rekursive Lösung eines Problems hübscher. Das ist ein technischer Begriff, und darauf kommt es an.
quelle
A
, die etwas in einem Baum findet. JedesA
Mal, wenn es auf dieses Ding stößt, startet es eine andere rekursive Funktion,B
die an der Stelle, an der es gestartet wurde, ein verwandtes Ding im Teilbaum findetA
. SobaldB
die Rekursion beendet ist, zu der sie zurückkehrtA
, setzt diese ihre eigene Rekursion fort. Man kann einen Stack fürA
und einen für deklarierenB
oder denB
Stack in dieA
Schleife legen . Wenn man darauf besteht, einen einzigen Stapel zu verwenden, werden die Dinge wirklich kompliziert.Therefore, the one thing recursion can do that loops can't is make some tasks super easy.
Und das einzige, was Schleifen tun können, ist, dass einige Aufgaben sehr einfach sind. Haben Sie die hässlichen, nicht intuitiven Dinge gesehen, die Sie tun müssen, um die meisten natürlich iterativen Probleme von naiver Rekursion zu Schwanzrekursion umzuwandeln, damit sie den Stapel nicht sprengen?map
oderfold
(in der Tat , wenn Sie sich dazu entscheiden , sie Primitiven zu betrachten, ich glaube , Sie verwenden könnenfold
/unfold
als dritte Alternative zu Schleifen oder Rekursionen verwenden). Wenn Sie keinen Bibliothekscode schreiben, gibt es nicht so viele Fälle, in denen Sie sich über die Implementierung der Iteration Gedanken machen sollten, anstatt über die Aufgabe, die sie ausführen soll. In der Praxis bedeutet dies, dass explizite Schleifen und explizite Rekursionen gleichermaßen schlecht sind Abstraktionen, die auf oberster Ebene vermieden werden sollten.Nein.
Erst bis hinunter zu den sehr Grundlagen des notwendigen Minimums , um eine Schleife zu berechnen, müssen Sie nur in der Lage sein (dies allein nicht ausreicht, sondern ist eine notwendige Komponente). Es ist egal wie .
Jede Programmiersprache, die eine Turing-Maschine implementieren kann, heißt Turing complete . Und es gibt viele Sprachen, die komplett sind.
Meine Lieblingssprache auf dem Weg dorthin von "das funktioniert tatsächlich?" Turing Vollständigkeit ist die von FRACTRAN , die Turing vollständig ist . Es hat eine Schleifenstruktur und Sie können eine Turing-Maschine darin implementieren. Somit kann alles, was berechenbar ist, in einer Sprache implementiert werden, die keine Rekursion aufweist. Daher gibt es nichts , was Ihnen eine Rekursion in Bezug auf die Berechenbarkeit bieten kann, was eine einfache Schleife nicht kann.
Das läuft wirklich auf ein paar Punkte hinaus:
Das soll nicht heißen, dass es einige Problemklassen gibt, die eher mit Rekursion als mit Schleifen oder eher mit Schleifen als mit Rekursion betrachtet werden können. Diese Tools sind jedoch ebenso leistungsfähig.
Und während ich das auf die Spitze getrieben habe (vor allem, weil man Dinge findet, die Turing komplett und auf seltsame Weise implementiert), heißt das nicht, dass die Esolangs auf keinen Fall optional sind. Es gibt eine ganze Liste von Dingen, die versehentlich komplett sind einschließlich Magic the Gathering, Sendmail, MediaWiki-Vorlagen und dem Scala-Typensystem. Viele davon sind alles andere als optimal, wenn es darum geht, tatsächlich etwas Praktisches zu tun. Sie können lediglich alles berechnen, was mit diesen Tools berechenbar ist.
Diese Äquivalenz kann besonders interessant werden, wenn Sie sich mit einer bestimmten Art von Rekursion befassen, die als Tail Call bezeichnet wird .
Wenn Sie beispielsweise eine Fakultätsmethode haben, die wie folgt geschrieben wurde:
Diese Art der Rekursion wird als Schleife umgeschrieben - es wird kein Stapel verwendet. Solche Ansätze sind in der Tat oft eleganter und leichter zu verstehen als die zu schreibende äquivalente Schleife, aber auch hier kann für jeden rekursiven Aufruf eine äquivalente Schleife geschrieben werden und für jede Schleife kann ein rekursiver Aufruf geschrieben werden.
Es gibt auch Zeiten , in denen die einfache Schleife in einen Endaufruf rekursiven Aufruf Umwandlung kann verworren und seine mehr schwer zu verstehen.
Wenn Sie sich mit der Theorie befassen möchten, lesen Sie die These von Church Turing . Sie können auch die kirchliche These auf CS.SE als nützlich erachten.
quelle
Sie können einen rekursiven Algorithmus jederzeit in eine Schleife umwandeln, die eine Last-In-First-Out-Datenstruktur (AKA-Stapel) zum Speichern des temporären Zustands verwendet, da ein rekursiver Aufruf genau das ist: Speichern des aktuellen Zustands in einem Stapel, Fortsetzen des Algorithmus. dann später den Zustand wiederherstellen. Die kurze Antwort lautet also: Nein, solche Fälle gibt es nicht .
Es kann jedoch ein Argument für "Ja" angegeben werden. Nehmen wir ein konkretes, einfaches Beispiel: Sortieren zusammenführen. Sie müssen die Daten in zwei Teile teilen, die Teile zusammenführen, sortieren und dann kombinieren. Selbst wenn Sie keinen eigentlichen Funktionsaufruf für die Programmiersprache ausführen, um die Teile zusammenzuführen, müssen Sie Funktionen implementieren, die mit denen eines tatsächlichen Funktionsaufrufs identisch sind (Push-Status auf Ihren eigenen Stack, Sprung zu Start der Schleife mit verschiedenen Startparametern, dann später den Zustand von Ihrem Stapel ziehen).
Ist es Rekursion, wenn Sie den Rekursionsaufruf selbst implementieren, als separate Schritte "Push-Status" und "Zum Anfang springen" und "Pop-Status"? Und die Antwort darauf lautet: Nein, es wird immer noch nicht als Rekursion bezeichnet, sondern als Iteration mit explizitem Stack (wenn Sie eine etablierte Terminologie verwenden möchten).
Beachten Sie, dass dies auch von der Definition von "Aufgabe" abhängt. Wenn es darum geht, zu sortieren, können Sie dies mit vielen Algorithmen tun, von denen viele keinerlei Rekursion benötigen. Wenn es die Aufgabe ist, einen bestimmten Algorithmus zu implementieren, z.
Betrachten wir also die Frage, ob es allgemeine Aufgaben gibt, für die es nur rekursionsähnliche Algorithmen gibt. Aus dem Kommentar von @WizardOfMenlo unter der Frage geht hervor, dass die Ackermann-Funktion ein einfaches Beispiel dafür ist. Das Konzept der Rekursion steht also für sich, auch wenn es mit einem anderen Computerprogrammkonstrukt (Iteration mit explizitem Stack) implementiert werden kann.
quelle
Es kommt darauf an, wie streng Sie "Rekursion" definieren.
Wenn wir unbedingt verlangen, dass der Aufruf-Stack einbezogen wird (oder welcher Mechanismus zur Aufrechterhaltung des Programmstatus auch immer verwendet wird), können wir ihn jederzeit durch etwas ersetzen, das dies nicht tut. In der Tat haben Sprachen, die auf natürliche Weise zu einer starken Nutzung der Rekursion führen, in der Regel Compiler, die die Tail-Call-Optimierung stark nutzen. Was Sie also schreiben, ist rekursiv, was Sie ausführen, ist iterativ.
Betrachten wir jedoch einen Fall, in dem wir einen rekursiven Aufruf ausführen und das Ergebnis eines rekursiven Aufrufs für diesen rekursiven Aufruf verwenden.
Es ist einfach, den ersten rekursiven Aufruf zu wiederholen:
Wir können dann
goto
die Velociraptoren und den Schatten von Dijkstra entfernen , um sie abzuwehren :Um die anderen rekursiven Aufrufe zu entfernen, müssen wir die Werte einiger Aufrufe in einem Stapel speichern:
Wenn wir nun den Quellcode betrachten, haben wir unsere rekursive Methode zweifellos in eine iterative Methode umgewandelt.
In Anbetracht dessen, wozu dies kompiliert wurde, haben wir Code, der den Aufrufstapel verwendet, um eine Rekursion in Code zu implementieren, der dies nicht tut, umgewandelt (und dabei Code, der eine Stapelüberlauf-Ausnahme für selbst recht kleine Werte in Code auslöst, der nur wird) es dauert unglaublich lange, bis ich zurückkomme [siehe Wie kann ich verhindern, dass meine Ackerman-Funktion den Stapel überläuft? für einige weitere Optimierungen, die dafür sorgen, dass er tatsächlich für viele weitere mögliche Eingaben zurückkommt).
In Anbetracht der allgemeinen Implementierung der Rekursion haben wir Code, der den Aufrufstapel verwendet, in Code umgewandelt, der einen anderen Stapel verwendet, um ausstehende Vorgänge zu speichern. Wir könnten daher argumentieren, dass es immer noch rekursiv ist, wenn man es auf diesem niedrigen Niveau betrachtet.
Und auf dieser Ebene gibt es in der Tat keine anderen Möglichkeiten. Wenn Sie diese Methode als rekursiv betrachten, dann gibt es tatsächlich Dinge, die wir ohne sie nicht tun können. Im Allgemeinen wird ein solcher Code jedoch nicht als rekursiv bezeichnet. Der Begriff Rekursion ist nützlich, weil er eine Reihe von Ansätzen abdeckt und uns die Möglichkeit gibt, darüber zu sprechen, und wir verwenden keinen von ihnen mehr.
All dies setzt natürlich voraus, dass Sie die Wahl haben. Es gibt sowohl Sprachen, die rekursive Aufrufe verbieten, als auch Sprachen, denen die für die Iteration erforderlichen Schleifenstrukturen fehlen.
quelle
Die klassische Antwort lautet "Nein", aber lassen Sie mich näher erläutern, warum ich "Ja" für eine bessere Antwort halte.
Bevor wir fortfahren, lassen Sie uns etwas aus dem Weg räumen: Vom Standpunkt der Berechenbarkeit und Komplexität aus:
Okay, jetzt lass uns einen Fuß in die Praxis setzen, den anderen Fuß in die Theorie.
Der Aufrufstapel ist eine Kontrollstruktur , während ein manueller Stapel eine Datenstruktur ist. Kontrolle und Daten sind keine gleichen Konzepte, aber sie sind in dem Sinne äquivalent, dass sie in Bezug auf Berechenbarkeit oder Komplexität auf einander reduziert (oder über einander "emuliert") werden können.
Wann könnte diese Unterscheidung von Bedeutung sein? Wenn Sie mit Tools aus der Praxis arbeiten. Hier ist ein Beispiel:
Angenommen, Sie implementieren N-way
mergesort
. Möglicherweise haben Sie einefor
Schleife, die jedes derN
Segmente durchläuft ,mergesort
sie separat aufruft und dann die Ergebnisse zusammenführt.Wie könnten Sie dies mit OpenMP parallelisieren?
Im rekursiven Bereich ist es extrem einfach: einfach ausgedrückt
#pragma omp parallel for
Ihre Schleife um, die von 1 nach N geht, und Sie sind fertig. Im iterativen Bereich können Sie dies nicht tun. Sie müssen Threads manuell erzeugen und ihnen die entsprechenden Daten manuell übergeben, damit sie wissen, was zu tun ist.Auf der anderen Seite gibt es andere Tools (wie z. B. automatische Vektorisierer
#pragma vector
), die mit Schleifen arbeiten, bei Rekursion jedoch völlig unbrauchbar sind.Nur weil Sie beweisen können, dass die beiden Paradigmen mathematisch äquivalent sind, heißt das nicht, dass sie in der Praxis gleich sind. Ein Problem, das in einem Paradigma trivial zu automatisieren ist (z. B. Schleifenparallelisierung), ist im anderen Paradigma möglicherweise viel schwieriger zu lösen.
Das heißt: Werkzeuge für ein Paradigma werden nicht automatisch in andere Paradigmen übersetzt.
Wenn Sie ein Tool zur Lösung eines Problems benötigen, funktioniert das Tool möglicherweise nur mit einer bestimmten Herangehensweise. Infolgedessen können Sie das Problem nicht mit einer anderen Herangehensweise lösen, auch wenn Sie dies mathematisch nachweisen können so oder so gelöst werden.
quelle
Schauen wir uns neben den theoretischen Überlegungen an, wie Rekursionen und Schleifen aus der Sicht einer (Hardware- oder virtuellen) Maschine aussehen. Rekursion ist eine Kombination aus Kontrollfluss, mit der die Ausführung eines Codes gestartet und nach Abschluss (in einer vereinfachten Ansicht, wenn Signale und Ausnahmen ignoriert werden) und Daten, die an diesen anderen Code (Argumente) übergeben und von diesem zurückgegeben werden, zurückgegeben werden kann es (Ergebnis). In der Regel ist keine explizite Speicherverwaltung erforderlich, es wird jedoch implizit Stapelspeicher zugewiesen, um Rücksprungadressen, Argumente, Ergebnisse und lokale Zwischendaten zu speichern.
Eine Schleife ist eine Kombination aus Kontrollfluss und lokalen Daten. Vergleicht man dies mit einer Rekursion, so sieht man, dass die Datenmenge in diesem Fall festgelegt ist. Die einzige Möglichkeit, diese Einschränkung aufzuheben, ist die Verwendung von dynamischem Speicher (auch als Heap bezeichnet ), der bei Bedarf zugewiesen (und freigegeben) werden kann.
Zusammenfassen:
Unter der Annahme, dass der Steuerungsablaufteil einigermaßen leistungsfähig ist, besteht der einzige Unterschied in den verfügbaren Speichertypen. Wir haben also noch 4 Fälle (die Aussagekraft ist in Klammern angegeben):
Wenn die Spielregeln etwas strenger sind und die rekursive Implementierung keine Schleifen verwenden darf, erhalten wir stattdessen Folgendes:
Der Hauptunterschied zum vorherigen Szenario besteht darin, dass aufgrund des fehlenden Stapelspeichers bei einer Rekursion ohne Schleifen während der Ausführung nicht mehr Schritte ausgeführt werden können als in Codezeilen.
quelle
Ja. Es gibt mehrere allgemeine Aufgaben, die mit der Rekursion leicht zu erledigen sind, mit einfachen Schleifen jedoch nicht möglich sind:
quelle
Es gibt einen Unterschied zwischen rekursiven Funktionen und primitiven rekursiven Funktionen. Primitive rekursive Funktionen sind solche, die unter Verwendung von Schleifen berechnet werden, wobei die maximale Iterationszahl jeder Schleife berechnet wird, bevor die Schleifenausführung beginnt. (Und "rekursiv" hat hier nichts mit der Verwendung von Rekursion zu tun).
Primitive rekursive Funktionen sind weniger leistungsfähig als rekursive Funktionen. Das gleiche Ergebnis würden Sie erhalten, wenn Sie Funktionen verwenden, die die Rekursion verwenden, wobei die maximale Tiefe der Rekursion im Voraus berechnet werden muss.
quelle
Wenn Sie in c ++ programmieren und c ++ 11 verwenden, müssen Sie eine Sache mit Rekursionen erledigen: constexpr-Funktionen. Der Standard begrenzt dies jedoch auf 512, wie in dieser Antwort erläutert . Die Verwendung von Schleifen ist in diesem Fall nicht möglich, da in diesem Fall die Funktion nicht constexpr sein kann, dies wird jedoch in c ++ 14 geändert.
quelle
quelle
Ich stimme den anderen Fragen zu. Es gibt nichts, was Sie mit Rekursion tun können, was Sie mit einer Schleife nicht tun können.
ABER meiner Meinung nach kann eine Rekursion sehr gefährlich sein. Erstens ist es für einige schwieriger zu verstehen, was tatsächlich im Code passiert. Zweitens hat jeder Rekursionsschritt, zumindest für C ++ (ich bin mir nicht sicher, ob Java), eine Auswirkung auf den Speicher, da jeder Methodenaufruf eine Speicherakkumulation und Initialisierung des Methodenkopfs verursacht. Auf diese Weise können Sie Ihren Stapel sprengen. Versuchen Sie einfach die Rekursion der Fibonacci-Zahlen mit einem hohen Eingabewert.
quelle