Verstehen, wie rekursive Funktionen funktionieren

115

Wie der Titel erklärt, habe ich eine sehr grundlegende Programmierfrage, die ich bisher noch nicht beantworten konnte. Herausfiltern aller (äußerst cleveren) "Um die Rekursion zu verstehen, müssen Sie zuerst die Rekursion verstehen." Antworten aus verschiedenen Online-Threads Ich verstehe es immer noch nicht ganz.

Wenn ich verstehe, dass wir, wenn wir nicht wissen, was wir nicht wissen, dazu neigen können, die falschen Fragen zu stellen oder die richtigen Fragen falsch zu stellen, werde ich mitteilen, was ich "denke". Meine Frage ist in der Hoffnung, dass jemand mit einer ähnlichen Einstellung einige teilen kann Ein bisschen Wissen, das mir hilft, die rekursive Glühbirne einzuschalten!

Hier ist die Funktion (die Syntax ist in Swift geschrieben):

func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a: a + 1, b: b)
    }
}

Wir werden 2 und 5 als unsere Argumente verwenden:

println(sumInts(a: 2, b: 5))

Die Antwort lautet natürlich 14. Aber mir ist nicht klar, wie dieser Wert erreicht wird.

Dies sind meine 2 Aufhängevorgänge:

  1. Die Funktion wird rekursiv aufgerufen, bis eine Bedingung erfüllt ist. Diese Bedingung ist a> b. Wenn diese Bedingung erfüllt ist, geben Sie 0 zurück. Auf den ersten Blick würde ich erwarten, dass der Rückgabewert 0 ist, was offensichtlich falsch ist.

  2. Das Ausdrucken des Wertes von 'a' bei jeder Iteration ergibt einen Wert, den ich erwarten würde: 2, 3, 4, 5 (an welchem ​​Punkt 5 + 1> b, der die erste Bedingung erfüllt: a> b), aber ich nicht ' Ich sehe nicht, wie der Wert 14 erreicht wird.

Mein erster Gedanke ist, dass etwas Ähnliches wie das Magische geschieht:

var answer = a;
answer += a+1 until a > b;
return answer;   

Wenn ich also Magie ausschließe, bekomme ich einfach nichts. Ich würde gerne verstehen, was mehr als nur implizit passiert.

Wenn jemand freundlich erklären könnte, was technisch während dieser Art von Funktion passiert und warum das Ergebnis nicht 0 ist und wie a + sumInts(a: a + 1, b: b) = 14ich letztendlich für immer in Ihrer Schuld stehen würde.

Jason Elwood
quelle
7
Rekursion ist eines dieser Programmierkonzepte, die mathematisch viel einfacher zu verstehen sind als Code. es gibt eine gute Definition hier
BLGT
5
LearnYouARecursion, komplette Problemstellungen von Weltklasse-Professor!
recursion.ninja
15
Ich habe gerade haben Sie drängen „Recursion“ in das Google - Suchfeld eingeben. Eines dieser Ostereier. Ich werde dir die Überraschung nicht verderben.
Floris
7
Mögliches Duplikat von stackoverflow.com/questions/25676961/…
Neil McGuigan

Antworten:

107

Ich denke, die Verwirrung rührt daher, dass man es als "dieselbe Funktion" betrachtet, die oft aufgerufen wird. Wenn Sie sich vorstellen, dass "viele Kopien derselben Funktion aufgerufen werden", ist dies möglicherweise klarer:

Nur eine Kopie der Funktion gibt jemals 0 zurück, und es ist nicht die erste (es ist die letzte). Das Ergebnis des Aufrufs des ersten ist also nicht 0.

Für die zweite Verwirrung denke ich, dass es einfacher sein wird, die Rekursion auf Englisch zu formulieren. Lesen Sie diese Zeile:

return a + sumInts(a + 1, b: b)

als "den Wert von 'a' plus zurückgeben (der Rückgabewert einer anderen Kopie der Funktion, der der Wert der Kopie von 'a' plus ist (der Rückgabewert einer anderen Kopie der Funktion, der der Wert der zweiten Kopie von 'ist). a 'plus (... ", wobei jede Kopie der Funktion eine neue Kopie von sich selbst mit einer um 1 erhöhten erzeugt, bis die Bedingung a> b erfüllt ist.

Wenn Sie die Bedingung a> b erreichen, die wahr ist, haben Sie einen (möglicherweise willkürlich) langen Stapel von Kopien der Funktion, die sich gerade in der Ausführung befinden und alle auf das Ergebnis der nächsten Kopie warten, um herauszufinden, was sie sind sollte zu 'a' hinzufügen.

(Bearbeiten: Außerdem ist zu beachten, dass der Stapel von Kopien der von mir erwähnten Funktion eine echte Sache ist, die echten Speicher beansprucht und Ihr Programm zum Absturz bringt, wenn es zu groß wird. Der Compiler kann es in einigen Fällen optimieren Fälle, aber die Erschöpfung des Stapelspeichers ist eine signifikante und unglückliche Einschränkung rekursiver Funktionen in vielen Sprachen.

Catfish_Man
quelle
7
Catfish_Man: Ich denke du hast es geschafft! Es ist absolut sinnvoll, es als mehrere "Kopien" derselben Funktion zu betrachten. Ich wickle immer noch meinen Kopf darum, aber ich denke, du hast mich auf den richtigen Weg geschickt! Vielen Dank, dass Sie sich die Zeit genommen haben, einem anderen Programmierer zu helfen! Ich werde Ihre Antwort als die richtige Antwort markieren. Ich wünsche ihnen einen wunderbaren Tag!
Jason Elwood
13
Dies ist eine gute Analogie - obwohl Sie darauf achten sollten, sie nicht zu wörtlich zu nehmen, da jede "Kopie" tatsächlich genau der gleiche Code ist. Was für jede Kopie anders ist, sind alle Daten, an denen sie arbeitet.
Tim B
2
Ich bin nicht sehr glücklich darüber, es als Kopie zu betrachten. Ich finde, dass eine intuitivere Erklärung darin besteht, die Funktion selbst (den Code, was er tut) und einen Funktionsaufruf (Instanziierung dieser Funktion) zu unterscheiden, dem ein Stapelrahmen / Ausführungskontext zugeordnet ist. Die Funktion besitzt keine lokalen Variablen, sie werden beim Aufrufen (Aufrufen) der Funktion instanziiert. Aber ich denke, dies wird als Einführung in die Rekursion dienen
Thomas
5
Die korrekte Terminologie lautet, dass die Funktion mehrere Aufrufe enthält . Jeder Aufruf hat seine eigenen Instanzen von Variablen aund b.
Theodore Norvell
6
Ja, es gibt eine erhebliche Genauigkeit, die dieser Antwort hinzugefügt werden könnte. Ich habe absichtlich die Unterscheidung zwischen "Instanzen einer Funktion" und "Aktivierungsaufzeichnungen von Aufrufen einer einzelnen Funktion" weggelassen, weil es eine zusätzliche konzeptionelle Belastung war, die nicht wirklich zum Verständnis des Problems beiträgt. Es hilft beim Verständnis anderer Probleme, daher sind es immer noch nützliche Informationen, nur an anderer Stelle. Diese Kommentare scheinen ein guter Ort dafür zu sein :)
Catfish_Man
130

1.Die Funktion wird rekursiv aufgerufen, bis eine Bedingung erfüllt ist. Dieser Zustand ist a > b. Wenn diese Bedingung erfüllt ist, geben Sie 0 zurück. Auf den ersten Blick würde ich erwarten, dass der Rückgabewert 0 ist, was offensichtlich falsch ist.

Folgendes sumInts(2,5)würde der Computercomputer denken, wenn er in der Lage wäre:

I want to compute sumInts(2, 5)
for this, I need to compute sumInts(3, 5)
and add 2 to the result.
  I want to compute sumInts(3, 5)
  for this, I need to compute sumInts(4, 5)
  and add 3 to the result.
    I want to compute sumInts(4, 5)
    for this, I need to compute sumInts(5, 5)
    and add 4 to the result.
      I want to compute sumInts(5, 5)
      for this, I need to compute sumInts(6, 5)
      and add 5 to the result.
        I want to compute sumInts(6, 5)
        since 6 > 5, this is zero.
      The computation yielded 0, therefore I shall return 5 = 5 + 0.
    The computation yielded 5, therefore I shall return 9 = 4 + 5.
  The computation yielded 9, therefore I shall return 12 = 3 + 9.
The computation yielded 12, therefore I shall return 14 = 2 + 12.

Wie Sie sehen, gibt ein Aufruf der Funktion sumIntstatsächlich 0 zurück, dies ist jedoch nicht der endgültige Wert, da der Computer noch 5 zu dieser 0, dann 4 zum Ergebnis, dann 3 und dann 2 hinzufügen muss, wie in den vier letzten Sätzen von beschrieben die Gedanken unseres Computers. Beachten Sie, dass der Computer bei der Rekursion nicht nur den rekursiven Aufruf berechnen muss, sondern sich auch merken muss, was mit dem vom rekursiven Aufruf zurückgegebenen Wert zu tun ist. Es gibt einen speziellen Bereich im Computerspeicher, den Stapel, in dem diese Art von Informationen gespeichert wird. Dieser Speicherplatz ist begrenzt und zu rekursive Funktionen können den Stapel erschöpfen: Dies ist der Stapelüberlauf , der unserer beliebtesten Website ihren Namen gibt.

Ihre Aussage scheint implizit davon auszugehen, dass der Computer bei einem rekursiven Aufruf vergisst, was er war, aber dies ist nicht der Fall. Deshalb stimmt Ihre Schlussfolgerung nicht mit Ihrer Beobachtung überein.

2.Das Ausdrucken des Wertes von 'a' bei jeder Iteration ergibt einen Wert, den ich erwarten würde: 2, 3, 4, 5 (an welchem ​​Punkt 5 + 1> b die erste Bedingung erfüllt: a> b), aber ich immer noch sehe nicht, wie der Wert von 14 erreicht wird.

Dies liegt daran, dass der Rückgabewert kein aSelbst ist, sondern die Summe aus dem Wert aund dem Wert, der vom rekursiven Aufruf zurückgegeben wird.

Michael Le Barbier Grünewald
quelle
3
Vielen Dank, dass Sie sich die Zeit genommen haben, diese großartige Antwort zu schreiben, Michael! +1!
Jason Elwood
9
@JasonElwood Vielleicht ist es hilfreich, wenn Sie sumIntsso ändern , dass die „Computergedanken“ tatsächlich aufgeschrieben werden. Sobald Sie eine Hand solcher Funktionen geschrieben haben, werden Sie es wahrscheinlich "verstanden" haben!
Michael Le Barbier Grünewald
4
Dies ist eine gute Antwort, obwohl ich fest , dass es keine Anforderung , dass Platz Funktion Aktivierung trifft auf einer Datenstruktur „der Stapel“ genannt. Die Rekursion könnte durch einen Continuation-Passing-Stil implementiert werden. In diesem Fall gibt es überhaupt keinen Stapel. Der Stapel ist nur eine - besonders effiziente und daher allgemein gebräuchliche - Bestätigung des Begriffs der Fortsetzung.
Eric Lippert
1
@EricLippert Obwohl Techniken zur Implementierung von Rekursivität per se ein interessantes Thema sind , bin ich mir nicht sicher, ob es für das OP - das verstehen möchte, wie es funktioniert “nützlich wäre, der Vielfalt der verwendeten Mechanismen ausgesetzt zu sein. Während Continuation-Passing-Stil oder auf Erweiterungen basierende Sprachen (z. B. TeX und m4) an sich nicht schwieriger sind als gängigere Programmierparadigmen, werde ich niemanden beleidigen, indem ich diese als „exotisch“ bezeichne und eine kleine Notlüge wie „es passiert immer auf dem Stapel“ sollte helfen Sie dem OP, das Konzept zu verstehen. (Und eine Art Stapel ist immer beteiligt.)
Michael Le Barbier Grünewald
1
Es muss eine Möglichkeit geben, dass sich die Software daran erinnert, was sie getan hat, die Funktion rekursiv aufruft und dann bei ihrer Rückkehr in den ursprünglichen Zustand zurückkehrt. Dieser Mechanismus wirkt stapelartig, daher ist es praktisch, ihn als Stapel zu bezeichnen, selbst wenn eine andere Datenstruktur verwendet wird.
Barmar
48

Um die Rekursion zu verstehen, müssen Sie das Problem anders betrachten. Anstelle einer großen logischen Folge von Schritten, die als Ganzes Sinn macht, nehmen Sie stattdessen ein großes Problem und teilen sich in kleinere Probleme auf und lösen diese. Sobald Sie eine Antwort auf die Unterprobleme haben, kombinieren Sie die Ergebnisse der Unterprobleme, um das zu ergeben Lösung für das größere Problem. Denken Sie an Sie und Ihre Freunde, die die Anzahl der Murmeln in einem riesigen Eimer zählen müssen. Sie nehmen jeweils einen kleineren Eimer und zählen diese einzeln. Wenn Sie fertig sind, addieren Sie die Summen. Nun, wenn jeder von Ihnen einen Freund findet und die Eimer weiter aufteilt, müssen Sie nur noch auf diese anderen Freunde warten Finde ihre Summen heraus, bring sie jedem von dir zurück, du addierst sie. Und so weiter.

Sie müssen sich daran erinnern, dass die Funktion jedes Mal, wenn sie sich rekursiv aufruft, einen neuen Kontext mit einer Teilmenge des Problems erstellt. Sobald dieser Teil behoben ist, wird er zurückgegeben, damit die vorherige Iteration abgeschlossen werden kann.

Lassen Sie mich Ihnen die Schritte zeigen:

sumInts(a: 2, b: 5) will return: 2 + sumInts(a: 3, b: 5)
sumInts(a: 3, b: 5) will return: 3 + sumInts(a: 4, b: 5)
sumInts(a: 4, b: 5) will return: 4 + sumInts(a: 5, b: 5)
sumInts(a: 5, b: 5) will return: 5 + sumInts(a: 6, b: 5)
sumInts(a: 6, b: 5) will return: 0

Sobald sumInts (a: 6, b: 5) ausgeführt wurde, können die Ergebnisse berechnet werden. Gehen Sie also die Kette mit den Ergebnissen zurück, die Sie erhalten:

 sumInts(a: 6, b: 5) = 0
 sumInts(a: 5, b: 5) = 5 + 0 = 5
 sumInts(a: 4, b: 5) = 4 + 5 = 9
 sumInts(a: 3, b: 5) = 3 + 9 = 12
 sumInts(a: 2, b: 5) = 2 + 12 = 14.

Eine andere Möglichkeit, die Struktur der Rekursion darzustellen:

 sumInts(a: 2, b: 5) = 2 + sumInts(a: 3, b: 5)
 sumInts(a: 2, b: 5) = 2 + 3 + sumInts(a: 4, b: 5)  
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + sumInts(a: 5, b: 5)  
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + sumInts(a: 6, b: 5)
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + 0
 sumInts(a: 2, b: 5) = 14 
rauben
quelle
2
Sehr gut ausgedrückt, Rob. Sie haben es so formuliert, dass es sehr klar und leicht zu verstehen ist. Danke, dass du dir die Zeit genommen hast!
Jason Elwood
3
Dies ist die klarste Darstellung dessen, was vor sich geht, ohne auf die Theorie und die technischen Details einzugehen, und zeigt jeden Schritt der Ausführung klar.
Bryan
2
Ich bin froh. :) Es ist nicht immer einfach, diese Dinge zu erklären. Danke für das Kompliment.
Rob
1
+1. So würde ich es beschreiben, speziell mit Ihrem letzten Beispiel der Struktur. Es ist hilfreich, das Geschehen visuell abzuwickeln.
KChaloux
40

Rekursion ist ein schwieriges Thema, und ich glaube nicht, dass ich es hier voll und ganz gerecht werden kann. Stattdessen werde ich versuchen, mich auf den bestimmten Code zu konzentrieren, den Sie hier haben, und sowohl die Intuition für die Funktionsweise der Lösung als auch die Mechanik der Berechnung des Ergebnisses durch den Code zu beschreiben.

Der Code, den Sie hier angegeben haben, löst das folgende Problem: Sie möchten die Summe aller Ganzzahlen von a bis b einschließlich kennen. Für Ihr Beispiel möchten Sie die Summe der Zahlen von 2 bis einschließlich 5, einschließlich

2 + 3 + 4 + 5

Wenn Sie versuchen, ein Problem rekursiv zu lösen, sollte einer der ersten Schritte darin bestehen, herauszufinden, wie das Problem in ein kleineres Problem mit derselben Struktur zerlegt werden kann. Nehmen wir also an, Sie wollten die Zahlen von 2 bis einschließlich 5 zusammenfassen. Eine Möglichkeit, dies zu vereinfachen, besteht darin, festzustellen, dass die obige Summe als umgeschrieben werden kann

2 + (3 + 4 + 5)

Hier ist (3 + 4 + 5) zufällig die Summe aller ganzen Zahlen zwischen 3 und 5 einschließlich. Mit anderen Worten, wenn Sie die Summe aller Ganzzahlen zwischen 2 und 5 wissen möchten, berechnen Sie zunächst die Summe aller Ganzzahlen zwischen 3 und 5 und addieren Sie dann 2.

Wie berechnet man also die Summe aller ganzen Zahlen zwischen 3 und 5 einschließlich? Nun, diese Summe ist

3 + 4 + 5

was stattdessen als gedacht werden kann

3 + (4 + 5)

Hier ist (4 + 5) die Summe aller ganzen Zahlen zwischen 4 und 5 einschließlich. Wenn Sie also die Summe aller Zahlen zwischen 3 und 5 einschließlich berechnen möchten, berechnen Sie die Summe aller ganzen Zahlen zwischen 4 und 5 und addieren Sie dann 3.

Hier gibt es ein Muster! Wenn Sie die Summe der ganzen Zahlen zwischen a und b einschließlich berechnen möchten, können Sie Folgendes tun. Berechnen Sie zunächst die Summe der ganzen Zahlen zwischen a + 1 und b einschließlich. Fügen Sie als Nächstes ein zu dieser Summe hinzu. Sie werden feststellen, dass "Berechnen Sie die Summe der ganzen Zahlen zwischen a + 1 und b einschließlich" so ziemlich das gleiche Problem ist, das wir bereits zu lösen versuchen, aber mit leicht unterschiedlichen Parametern. Anstatt von a nach b einschließlich zu berechnen, berechnen wir von a + 1 nach b einschließlich. Das ist der rekursive Schritt - um das größere Problem zu lösen ("Summe von a nach b, einschließlich"), reduzieren wir das Problem auf eine kleinere Version von sich selbst ("Summe von a + 1 bis einschließlich b").

Wenn Sie sich den Code oben ansehen, werden Sie feststellen, dass dieser Schritt darin enthalten ist:

return a + sumInts(a + 1, b: b)

Dieser Code ist einfach eine Übersetzung der obigen Logik. Wenn Sie von a nach b einschließlich summieren möchten, summieren Sie zunächst a + 1 bis einschließlich b (das ist der rekursive Aufruf von sumInts) und fügen Sie dann hinzu a.

Natürlich wird dieser Ansatz an sich nicht funktionieren. Wie würden Sie beispielsweise die Summe aller ganzen Zahlen zwischen 5 und einschließlich 5 berechnen? Nun, mit unserer aktuellen Logik würden Sie die Summe aller ganzen Zahlen zwischen 6 und einschließlich 5 berechnen und dann 5 addieren. Wie berechnen Sie also die Summe aller ganzen Zahlen zwischen einschließlich 6 und 5? Nun, mit unserer aktuellen Logik würden Sie die Summe aller ganzen Zahlen zwischen 7 und 5 einschließlich berechnen und dann 6 hinzufügen. Sie werden hier ein Problem bemerken - das geht einfach weiter und weiter!

Bei der rekursiven Problemlösung muss es eine Möglichkeit geben, die Vereinfachung des Problems zu beenden und es stattdessen direkt zu lösen. In der Regel finden Sie einen einfachen Fall, in dem die Antwort sofort ermittelt werden kann, und strukturieren dann Ihre Lösung, um einfache Fälle direkt zu lösen, wenn sie auftreten. Dies wird normalerweise als Basisfall oder rekursive Basis bezeichnet .

Was ist der Grund für dieses spezielle Problem? Wenn Sie ganze Zahlen von a bis einschließlich b aufsummieren und a größer als b ist, lautet die Antwort 0 - es gibt keine Zahlen im Bereich! Daher strukturieren wir unsere Lösung wie folgt:

  1. Wenn a> b, ist die Antwort 0.
  2. Andernfalls (a ≤ b) erhalten Sie die Antwort wie folgt:
    1. Berechnen Sie die Summe der ganzen Zahlen zwischen a + 1 und b.
    2. Fügen Sie ein hinzu, um die Antwort zu erhalten.

Vergleichen Sie nun diesen Pseudocode mit Ihrem tatsächlichen Code:

func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a + 1, b: b)
    }
}

Beachten Sie, dass zwischen der im Pseudocode beschriebenen Lösung und diesem tatsächlichen Code fast genau eine Eins-zu-Eins-Zuordnung besteht. Der erste Schritt ist der Basisfall. Wenn Sie nach der Summe eines leeren Zahlenbereichs fragen, erhalten Sie 0. Andernfalls berechnen Sie die Summe zwischen a + 1 und b und fügen Sie dann a hinzu.

Bisher habe ich nur eine allgemeine Idee hinter dem Code gegeben. Aber Sie hatten zwei andere, sehr gute Fragen. Erstens, warum gibt dies nicht immer 0 zurück, da die Funktion sagt, dass 0 zurückgegeben werden soll, wenn a> b? Zweitens, woher kommen die 14 eigentlich? Schauen wir uns diese der Reihe nach an.

Versuchen wir einen sehr, sehr einfachen Fall. Was passiert, wenn Sie anrufen sumInts(6, 5)? In diesem Fall sehen Sie beim Durchlaufen des Codes, dass die Funktion nur 0 zurückgibt. Das ist das Richtige, um - es gibt keine Zahlen im Bereich. Versuchen Sie es jetzt noch einmal. Was passiert, wenn Sie anrufen sumInts(5, 5)? Nun, hier ist was passiert:

  1. Du rufst an sumInts(5, 5). Wir fallen in den elseZweig, der den Wert von `a + sumInts (6, 5) zurückgibt.
  2. Um sumInts(5, 5)festzustellen, was sumInts(6, 5)ist, müssen wir pausieren, was wir tun, und einen Anruf tätigen sumInts(6, 5).
  3. sumInts(6, 5)wird gerufen. Es betritt den ifZweig und kehrt zurück 0. Diese Instanz von sumIntswurde jedoch von aufgerufen sumInts(5, 5), sodass der Rückgabewert an sumInts(5, 5)und nicht an den Anrufer der obersten Ebene zurückgemeldet wird.
  4. sumInts(5, 5)Jetzt kann man rechnen 5 + sumInts(6, 5), um zurück zu kommen 5. Es gibt es dann an den Anrufer der obersten Ebene zurück.

Beachten Sie, wie der Wert 5 hier gebildet wurde. Wir begannen mit einem aktiven Anruf bei sumInts. Dadurch wurde ein weiterer rekursiver Aufruf ausgelöst, und der von diesem Aufruf zurückgegebene Wert übermittelte die Informationen an sumInts(5, 5). Der Aufruf von sumInts(5, 5)dann führte wiederum einige Berechnungen durch und gab einen Wert an den Anrufer zurück.

Wenn Sie dies mit versuchen sumInts(4, 5), geschieht Folgendes:

  • sumInts(4, 5)versucht zurückzukehren 4 + sumInts(5, 5). Dazu ruft es auf sumInts(5, 5).
    • sumInts(5, 5)versucht zurückzukehren 5 + sumInts(6, 5). Dazu ruft es auf sumInts(6, 5).
    • sumInts(6, 5)gibt 0 zurück zu sumInts(5, 5).</li> <li>sumInts (5, 5) now has a value forsumInts (6, 5) , namely 0. It then returns5 + 0 = 5`.
  • sumInts(4, 5)hat jetzt einen Wert für sumInts(5, 5), nämlich 5. Es kehrt dann zurück 4 + 5 = 9.

Mit anderen Worten, der zurückgegebene Wert wird gebildet, indem die Werte einzeln aufsummiert werden, wobei jedes Mal ein Wert verwendet wird, der von einem bestimmten rekursiven Aufruf an zurückgegeben wird sumInts und der aktuelle Wert von addiert wird a. Wenn die Rekursion ihren Tiefpunkt erreicht, gibt der tiefste Aufruf 0 zurück. Dieser Wert verlässt die rekursive Aufrufkette jedoch nicht sofort. Stattdessen wird der Wert nur eine Ebene darüber an den rekursiven Aufruf zurückgegeben. Auf diese Weise fügt jeder rekursive Aufruf nur eine weitere Zahl hinzu und gibt sie weiter oben in der Kette zurück, was mit der Gesamtsumme gipfelt. Versuchen Sie als Übung, dies aufzuspüren sumInts(2, 5), womit Sie beginnen wollten.

Hoffe das hilft!

templatetypedef
quelle
3
Vielen Dank, dass Sie sich die Zeit genommen haben, eine so umfassende Antwort zu geben! Hier gibt es eine Menge großartiger Informationen, die mir helfen, mich mit rekursiven Funktionen vertraut zu machen, und die sicherlich anderen helfen werden, die in Zukunft über diesen Beitrag stolpern. Nochmals vielen Dank und einen schönen Tag!
Jason Elwood
22

Sie haben hier bisher einige gute Antworten, aber ich werde noch eine hinzufügen, die einen anderen Ansatz verfolgt.

Zunächst habe ich viele Artikel über einfache rekursive Algorithmen geschrieben, die Sie vielleicht interessant finden. sehen

http://ericlippert.com/tag/recursion/

http://blogs.msdn.com/b/ericlippert/archive/tags/recursion/

Diese befinden sich in der neuesten Reihenfolge. Beginnen Sie also von unten.

Zweitens haben bisher alle Antworten die rekursive Semantik unter Berücksichtigung der Funktionsaktivierung beschrieben . Jeder Aufruf führt eine neue Aktivierung durch , und der rekursive Aufruf wird im Rahmen dieser Aktivierung ausgeführt. Das ist eine gute Art, darüber nachzudenken, aber es gibt eine andere, äquivalente Art: intelligentes Text-Suchen und Ersetzen .

Lassen Sie mich Ihre Funktion in eine etwas kompaktere Form umschreiben. Betrachten Sie dies nicht als in einer bestimmten Sprache.

s = (a, b) => a > b ? 0 : a + s(a + 1, b)

Ich hoffe das ergibt Sinn. Wenn Sie mit dem bedingten Operator nicht vertraut sind, hat er die Form condition ? consequence : alternativeund seine Bedeutung wird klar.

Jetzt sind wir bewerten wollen s(2,5) wir tun, indem Sie einen Text mit der Funktion Körper des Anrufs ersetzt tun, ersetzen Sie dann amit 2und bmit 5:

s(2, 5) 
---> 2 > 5 ? 0 : 2 + s(2 + 1, 5)

Bewerten Sie nun die Bedingung. Wir ersetzen textlich 2 > 5mit false.

---> false ? 0 : 2 + s(2 + 1, 5)

Ersetzen Sie nun alle falschen Bedingungen in Textform durch die Alternative und alle wahren Bedingungen mit der Konsequenz. Wir haben nur falsche Bedingungen, daher ersetzen wir diesen Ausdruck textlich durch die Alternative:

---> 2 + s(2 + 1, 5)

+Um zu vermeiden, dass ich all diese Zeichen eingeben muss, ersetzen Sie die konstante Arithmetik in Textform durch ihren Wert. (Dies ist ein kleiner Betrug, aber ich möchte nicht alle Klammern im Auge behalten müssen!)

---> 2 + s(3, 5)

Suchen und ersetzen Sie nun, diesmal mit dem Textkörper für den Anruf, 3für aund 5für b. Wir werden den Ersatz für den Anruf in Klammern setzen:

---> 2 + (3 > 5 ? 0 : 3 + s(3 + 1, 5))

Und jetzt machen wir einfach weiter die gleichen Schritte zur Textersetzung:

---> 2 + (false ? 0 : 3 + s(3 + 1, 5))  
---> 2 + (3 + s(3 + 1, 5))                
---> 2 + (3 + s(4, 5))                     
---> 2 + (3 + (4 > 5 ? 0 : 4 + s(4 + 1, 5)))
---> 2 + (3 + (false ? 0 : 4 + s(4 + 1, 5)))
---> 2 + (3 + (4 + s(4 + 1, 5)))
---> 2 + (3 + (4 + s(5, 5)))
---> 2 + (3 + (4 + (5 > 5 ? 0 : 5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (false ? 0 : 5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (5 + s(6, 5))))
---> 2 + (3 + (4 + (5 + (6 > 5 ? 0 : s(6 + 1, 5)))))
---> 2 + (3 + (4 + (5 + (true ? 0 : s(6 + 1, 5)))))
---> 2 + (3 + (4 + (5 + 0)))
---> 2 + (3 + (4 + 5))
---> 2 + (3 + 9)
---> 2 + 12
---> 14

Alles, was wir hier getan haben, war nur eine einfache Textersetzung . Eigentlich hätte ich "2 + 1" und so weiter nicht durch "3" ersetzen sollen, bis ich musste, aber pädagogisch wäre es schwer zu lesen geworden.

Die Funktionsaktivierung ist nichts anderes als das Ersetzen des Funktionsaufrufs durch den Hauptteil des Aufrufs und das Ersetzen der formalen Parameter durch die entsprechenden Argumente. Sie müssen vorsichtig sein, wenn Sie Klammern intelligent einfügen, aber abgesehen davon handelt es sich nur um Textersetzung.

Natürlich implementieren die meisten Sprachen die Aktivierung nicht als Textersetzung, sondern logisch das, was es ist.

Was ist dann eine unbegrenzte Rekursion? Eine Rekursion, bei der die Textersetzung nicht aufhört! Beachten Sie, wie wir schließlich zu einem Schritt kamen, bei dem es keinen sErsatz mehr gab, und wir dann einfach die Regeln für die Arithmetik anwenden konnten.

Eric Lippert
quelle
Gutes Beispiel, aber es bricht Ihnen das Herz, wenn Sie komplexere Berechnungen durchführen. Z.B. Finden des gemeinsamen Vorfahren in einem Binärbaum.
CodeYogi
11

Die Art und Weise, wie ich normalerweise herausfinde, wie eine rekursive Funktion funktioniert, besteht darin, den Basisfall zu betrachten und rückwärts zu arbeiten. Hier ist diese Technik, die auf diese Funktion angewendet wird.

Zuerst der Basisfall:

sumInts(6, 5) = 0

Dann der Aufruf direkt darüber im Aufrufstapel :

sumInts(5, 5) == 5 + sumInts(6, 5)
sumInts(5, 5) == 5 + 0
sumInts(5, 5) == 5

Dann der Aufruf direkt darüber im Aufrufstapel:

sumInts(4, 5) == 4 + sumInts(5, 5)
sumInts(4, 5) == 4 + 5
sumInts(4, 5) == 9

Und so weiter:

sumInts(3, 5) == 3 + sumInts(4, 5)
sumInts(3, 5) == 3 + 9
sumInts(3, 5) == 12

Und so weiter:

sumInts(2, 5) == 2 + sumInts(3, 5)
sumInts(4, 5) == 2 + 12
sumInts(4, 5) == 14

Beachten Sie, dass wir zu unserem ursprünglichen Aufruf der Funktion gelangt sind sumInts(2, 5) == 14

Die Reihenfolge, in der diese Aufrufe ausgeführt werden:

sumInts(2, 5)
sumInts(3, 5)
sumInts(4, 5)
sumInts(5, 5)
sumInts(6, 5)

Die Reihenfolge, in der diese Anrufe zurückgegeben werden:

sumInts(6, 5)
sumInts(5, 5)
sumInts(4, 5)
sumInts(3, 5)
sumInts(2, 5)

Beachten Sie, dass wir zu einer Schlussfolgerung über die Funktionsweise der Funktion gekommen sind, indem wir die Aufrufe in der Reihenfolge verfolgt haben, in der sie zurückgegeben werden .

OregonTrail
quelle
5

Ich werde es versuchen.

Wenn ich die Gleichung a + sumInts (a + 1, b) ausführe, werde ich zeigen, wie die endgültige Antwort 14 ist.

//the sumInts function definition
func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a + 1, b)
    }
}

Given: a = 2 and b = 5

1) 2 + sumInts(2+1, 5)

2) sumInts(3, 5) = 12
   i) 3 + sumInts(3+1, 5)
   ii) 4 + sumInts(4+1, 5)
   iii) 5 + sumInts(5+1, 5)
   iv) return 0
   v) return 5 + 0
   vi) return 4 + 5
   vii) return 3 + 9

3) 2 + 12 = 14.

Lassen Sie uns wissen, wenn Sie weitere Fragen haben.

Hier ist ein weiteres Beispiel für rekursive Funktionen im folgenden Beispiel.

Ein Mann hat gerade das College abgeschlossen.

t ist die Zeit in Jahren.

Die tatsächliche Gesamtzahl der Jahre vor der Pensionierung kann wie folgt berechnet werden:

public class DoIReallyWantToKnow 
{
    public int howLongDoIHaveToWork(int currentAge)
    {
      const int DESIRED_RETIREMENT_AGE = 65;
      double collectedMoney = 0.00; //remember, you just graduated college
      double neededMoneyToRetire = 1000000.00

      t = 0;
      return work(t+1);
    }

    public int work(int time)
    {
      collectedMoney = getCollectedMoney();

      if(currentAge >= DESIRED_RETIREMENT_AGE 
          && collectedMoney == neededMoneyToRetire
      {
        return time;
      }

      return work(time + 1);
    }
}

Und das sollte gerade genug sein, um jemanden zu deprimieren, lol. ;-P

Bryan
quelle
5

Rekursion. In der Informatik wird die Rekursion unter dem Thema Endliche Automaten ausführlich behandelt.

In seiner einfachsten Form ist es eine Selbstreferenz. Zum Beispiel ist die Aussage "Mein Auto ist ein Auto" eine rekursive Aussage. Das Problem ist, dass die Anweisung eine unendliche Rekursion ist, da sie niemals enden wird. Die Definition in der Aussage eines "Autos" ist, dass es ein "Auto" ist, so dass es ersetzt werden kann. Es gibt jedoch kein Ende, denn im Falle einer Substitution wird es immer noch "Mein Auto ist ein Auto".

Dies könnte anders sein, wenn die Aussage lautet: "Mein Auto ist ein Bentley. Mein Auto ist blau." In diesem Fall könnte der Ersatz in der zweiten Situation für das Auto "Bentley" sein, was zu "Mein Bentley ist blau" führt. Diese Arten von Substitutionen werden in der Informatik durch kontextfreie Grammatiken mathematisch erklärt .

Die eigentliche Substitution ist eine Produktionsregel. Da die Aussage durch S dargestellt wird und das Auto eine Variable ist, die ein "Bentley" sein kann, kann diese Aussage rekursiv rekonstruiert werden.

S -> "my"S | " "S | CS | "is"S | "blue"S | ε
C -> "bentley"

Dies kann auf verschiedene Arten konstruiert werden, da jedes |bedeutet, dass es eine Wahl gibt. Skann durch eine dieser Auswahlmöglichkeiten ersetzt werden, und S beginnt immer leer. Die εMittel, um die Produktion zu beenden. So wie Ses ersetzt werden kann, können auch andere Variablen ersetzt werden (es gibt nur eine und diese Cwürde "bentley" darstellen).

Beginnen Sie also damit S, leer zu sein und es durch die erste Wahl "my"S Szu ersetzen

"my"S

Skann weiterhin ersetzt werden, da es eine Variable darstellt. Wir könnten wieder "mein" wählen oder ε, um es zu beenden, aber lassen Sie uns weiterhin unsere ursprüngliche Aussage machen. Wir wählen den Raum, der Sdurch ersetzt wird" "S

"my "S

Als nächstes wählen wir C.

"my "CS

Und C hat nur eine Wahl für den Austausch

"my bentley"S

Und der Platz wieder für S.

"my bentley "S

Und so weiter "my bentley is"S, "my bentley is "S, "my bentley is blue"S,"my bentley is blue" ( als Ersatz für S für ε beendet die Produktion) und wir haben rekursiv unsere Aussage „meine bentley ist blau“ gebaut.

Stellen Sie sich Rekursion als diese Produktionen und Ersetzungen vor. Jeder Schritt im Prozess ersetzt seinen Vorgänger, um das Endergebnis zu erzielen. Im genauen Beispiel der rekursiven Summe von 2 bis 5 erhalten Sie die Produktion

S -> 2 + A
A -> 3 + B
B -> 4 + C
C -> 5 + D
D -> 0

Das wird

2 + A
2 + 3 + B
2 + 3 + 4 + C
2 + 3 + 4 + 5 + D
2 + 3 + 4 + 5 + 0
14
Travis J.
quelle
Ich bin mir nicht sicher, ob endliche Zustandsautomaten oder kontextfreie Grammatiken die besten Beispiele sind, die helfen können, eine erste Intuition über Rekursion zu entwickeln. Sie sind schöne Beispiele, aber Programmierern ohne CS-Hintergrund vielleicht etwas unbekannt.
Chi
4

Ich denke, der beste Weg, um rekursive Funktionen zu verstehen, besteht darin, zu erkennen, dass sie rekursive Datenstrukturen verarbeiten. Aber in Ihrer ursprünglichen FunktionsumInts(a: Int, b: Int) , die die Summe der Zahlen von abis rekursiv berechnet b, scheint es sich jedoch nicht um eine rekursive Datenstruktur zu handeln. Versuchen wir eine leicht modifizierte Version, sumInts(a: Int, n: Int)in der angegeben nist, wie viele Zahlen Sie hinzufügen.

Jetzt ist sumInts rekursiv n, eine natürliche Zahl. Immer noch keine rekursiven Daten, oder? Nun, eine natürliche Zahl könnte unter Verwendung von Peano-Axiomen als rekursive Datenstruktur betrachtet werden:

enum Natural = {
    case Zero
    case Successor(Natural)
}

Also, 0 = Null, 1 = Nachfolger (Null), 2 = Nachfolger (Nachfolger (Null)) und so weiter.

Sobald Sie eine rekursive Datenstruktur haben, haben Sie die Vorlage für die Funktion. Für jeden nicht rekursiven Fall können Sie den Wert direkt berechnen. Für die rekursiven Fälle nehmen Sie an, dass die rekursive Funktion bereits funktioniert, und verwenden sie, um den Fall zu berechnen, aber das Argument zu dekonstruieren. Im Fall von Natural bedeutet dies, dass Succesor(n)wir anstelle von noder gleichwertig anstelle von nverwenden n - 1.

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
    if (n == 0) {
        // non recursive case
    } else {
        // recursive case. We use sumInts(..., n - 1)
    }
}

Jetzt ist die rekursive Funktion einfacher zu programmieren. Zunächst der Basisfall,n=0 . Was sollen wir zurückgeben, wenn wir keine Zahlen hinzufügen möchten? Die Antwort ist natürlich 0.

Was ist mit dem rekursiven Fall? Wenn wir nZahlen hinzufügen möchten, die mit beginnen, aund wir bereits eine funktionierende sumIntsFunktion haben, die funktioniert n-1? Nun, wir müssen hinzufügen , aund rufen Sie dann sumIntsmit a + 1, so dass wir am Ende mit:

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
    if (n == 0) {
        return 0
    } else {
        return a + sumInts(a + 1, n - 1)
    }
}

Das Schöne ist, dass Sie jetzt nicht mehr in der niedrigen Rekursionsstufe denken müssen. Sie müssen nur Folgendes überprüfen:

  • Für die Basisfälle der rekursiven Daten wird die Antwort ohne Rekursion berechnet.
  • Für die rekursiven Fälle der rekursiven Daten wird die Antwort mithilfe der Rekursion über die zerstörten Daten berechnet.
jdinunzio
quelle
4

Sie könnten an der Implementierung von Funktionen durch Nisan und Schocken interessiert sein . Das verlinkte PDF ist Teil eines kostenlosen Online-Kurses. Es beschreibt den zweiten Teil einer Implementierung einer virtuellen Maschine, in dem der Schüler einen Compiler für die Sprache einer virtuellen Maschine zu einer Maschinensprache schreiben sollte. Die von ihnen vorgeschlagene Funktionsimplementierung kann rekursiv ausgeführt werden, da sie stapelbasiert ist.

So führen Sie in die Funktionsimplementierung ein: Beachten Sie den folgenden Code der virtuellen Maschine:

Geben Sie hier die Bildbeschreibung ein

Wenn Swift in diese Sprache der virtuellen Maschine kompiliert wurde, dann der folgende Swift-Codeblock:

mult(a: 2, b: 3) - 4

würde bis zu kompilieren

push constant 2  // Line 1
push constant 3  // Line 2
call mult        // Line 3
push constant 4  // Line 4
sub              // Line 5

Die Sprache der virtuellen Maschine basiert auf einem globalen Stapel .push constant nschiebt eine Ganzzahl auf diesen globalen Stapel.

Nach dem Ausführen der Zeilen 1 und 2 sieht der Stapel wie folgt aus:

256:  2  // Argument 0
257:  3  // Argument 1

256und 257sind Speicheradressen.

call mult schiebt die Rückleitungsnummer (3) auf den Stapel und reserviert Platz für die lokalen Variablen der Funktion.

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  0  // local 0

... und es geht zum Etikett function mult. Der darin enthaltene Code multwird ausgeführt. Als Ergebnis der Ausführung dieses Codes berechnen wir das Produkt aus 2 und 3, das in der 0. lokalen Variablen der Funktion gespeichert ist.

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0

Kurz bevor returnSie von mult kommen, werden Sie die Zeile bemerken:

push local 0  // push result

Wir werden das Produkt auf den Stapel schieben.

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0
260:  6  // product

Wenn wir zurückkehren, passiert Folgendes:

  • Fügen Sie den letzten Wert auf dem Stapel in die Speicheradresse des 0. Arguments ein (in diesem Fall 256). Dies ist der bequemste Ort, um es auszudrücken.
  • Verwerfen Sie alles auf dem Stapel bis zur Adresse des 0. Arguments.
  • Gehen Sie zur Rückleitungsnummer (in diesem Fall 3) und fahren Sie fort.

Nach der Rückkehr sind wir bereit, Zeile 4 auszuführen, und unser Stapel sieht folgendermaßen aus:

256:  6  // product that we just returned

Jetzt schieben wir 4 auf den Stapel.

256:  6
257:  4

subist eine primitive Funktion der Sprache der virtuellen Maschine. Es akzeptiert zwei Argumente und gibt das Ergebnis in der üblichen Adresse zurück: der des 0. Arguments.

Jetzt haben wir

256:  2  // 6 - 4 = 2

Nachdem Sie nun wissen, wie ein Funktionsaufruf funktioniert, ist es relativ einfach zu verstehen, wie die Rekursion funktioniert. Keine Magie , nur ein Stapel.

Ich habe Ihre sumIntsFunktion in dieser Sprache der virtuellen Maschine implementiert:

function sumInts 0     // `0` means it has no local variables.
  label IF
    push argument 0
    push argument 1
    lte              
    if-goto ELSE_CASE
    push constant 0
    return
  label ELSE_CASE
    push constant 2
    push argument 0
    push constant 1
    add
    push argument 1
    call sumInts       // Line 15
    add                // Line 16
    return             // Line 17
// End of function

Jetzt werde ich es nennen:

push constant 2
push constant 5
call sumInts           // Line 21

Der Code wird ausgeführt und wir gelangen bis zum Haltepunkt, an dem wir ltezurückkehren false. So sieht der Stapel an dieser Stelle aus:

// First invocation
256:  2   // argument 0
257:  5   // argument 1
258:  21  // return line number
259:  2   // augend
// Second
260:  3   // argument 0
261:  5   // argument 1
262:  15  // return line number
263:  3   // augend
// Third
264:  4   // argument 0
265:  5   // argument 1
266:  15  // return line number
267:  4   // augend
// Fourth
268:  5   // argument 0
269:  5   // argument 1
270:  15  // return line number
271:  5   // augend
// Fifth
272:  6   // argument 0
273:  5   // argument 1
274:  15  // return line number
275:  0   // return value

Lassen Sie uns nun unsere Rekursion "abwickeln". return0 und gehe zu Zeile 15 und gehe weiter.

271:  5
272:  0

Zeile 16: add

271:  5

Zeile 17: return5 und gehe zu Zeile 15 und gehe weiter.

267:  4
268:  5

Zeile 16: add

267:  9

Zeile 17: return9 und gehe zu Zeile 15 und gehe weiter.

263:  3
264:  9

Zeile 16: add

263:  12

Linie 17: return12 und gehe zu Linie 15 und gehe weiter.

259:  2
260:  12

Zeile 16: add

259:  14

Linie 17: return14 und gehe zu Linie 21 und gehe weiter.

256:  14

Hier hast du es. Rekursion: Verherrlicht goto.

Jackson
quelle
4

Ein wirklich guter Tipp, den ich beim Lernen und Verstehen von Rekursion gefunden habe, ist, einige Zeit damit zu verbringen, eine Sprache zu lernen, die keine andere Form von Schleifenkonstruktion als Rekursion hat. Auf diese Weise erhalten Sie ein gutes Gefühl dafür, wie Sie Rekursion durch Übung verwenden können.

Ich folgte http://www.htdp.org/, das nicht nur ein Schema-Tutorial ist, sondern auch eine großartige Einführung in das Entwerfen von Programmen in Bezug auf Architektur und Design bietet.

Aber im Grunde müssen Sie etwas Zeit investieren. Ohne ein "festes" Verständnis der Rekursion erscheinen Ihnen bestimmte Algorithmen, wie z. B. das Zurückverfolgen, immer "hart" oder sogar "magisch". Also durchhalten. :-D

Ich hoffe das hilft und viel Glück!

Th3Minstr3l
quelle
3

Es gibt bereits viele gute Antworten. Trotzdem versuche ich es.
Beim Aufruf erhält eine Funktion einen zugewiesenen Speicherplatz , der auf dem Speicherplatz der Aufruferfunktion gestapelt ist . In diesem Speicherbereich behält die Funktion die an sie übergebenen Parameter, die Variablen und ihre Werte bei. Dieser Speicherplatz verschwindet zusammen mit dem endenden Rückruf der Funktion. Mit der Idee des Stapels wird nun der Speicherplatz der Aufruferfunktion aktiv.

Bei rekursiven Aufrufen werden mit derselben Funktion mehrere Speicherplätze übereinander gestapelt. Das ist alles. Die einfache Vorstellung, wie der Stapel im Speicher eines Computers funktioniert, sollte Sie durch die Vorstellung bringen, wie die Rekursion bei der Implementierung erfolgt.

Gulshan
quelle
3

Ein bisschen abseits des Themas, ich weiß, aber ... versuchen Sie, die Rekursion in Google nachzuschlagen ... Sie werden anhand eines Beispiels sehen, was es bedeutet :-)


Frühere Versionen von Google haben den folgenden Text zurückgegeben (aus dem Speicher zitiert):

Rekursion

Siehe Rekursion

Am 10. September 2014 wurde der Witz über die Rekursion aktualisiert:

Rekursion

Meinten Sie: Rekursion


Eine weitere Antwort finden Sie in dieser Antwort .

Pierre Arnaud
quelle
3

Stellen Sie sich Rekursion als mehrere Klone vor , die dasselbe tun ...

Sie bitten, [1] zu klonen: "Summenzahlen zwischen 2 und 5"

+ clone[1]               knows that: result is 2 + "sum numbers between 3 and 5". so he asks to clone[2] to return: "sum numbers between 3 and 5"
|   + clone[2]           knows that: result is 3 + "sum numbers between 4 and 5". so he asks to clone[3] to return: "sum numbers between 4 and 5"
|   |   + clone[3]       knows that: result is 4 + "sum numbers between 5 and 5". so he asks to clone[4] to return: "sum numbers between 5 and 5"
|   |   |   + clone[4]   knows that: result is 5 + "sum numbers between 6 and 5". so he asks to clone[5] to return: "sum numbers between 6 and 5"
|   |   |   |   clone[5] knows that: he can't sum, because 6 is larger than 5. so he returns 0 as result.
|   |   |   + clone[4]   gets the result from clone[5] (=0)  and sums: 5 + 0,  returning 5
|   |   + clone[3]       gets the result from clone[4] (=5)  and sums: 4 + 5,  returning 9
|   + clone[2]           gets the result from clone[3] (=9)  and sums: 3 + 9,  returning 12
+ clone[1]               gets the result from clone[2] (=12) and sums: 2 + 12, returning 14

und voilá !!

Christian
quelle
2

Viele der obigen Antworten sind sehr gut. Eine nützliche Technik zum Lösen von Rekursionen besteht jedoch darin, zuerst zu formulieren, was wir tun möchten, und zu codieren, wie es ein Mensch lösen würde. Im obigen Fall wollen wir eine Folge aufeinanderfolgender Ganzzahlen (unter Verwendung der Zahlen von oben) zusammenfassen:

2, 3, 4, 5  //adding these numbers would sum to 14

Beachten Sie nun, dass diese Zeilen verwirrend sind (nicht falsch, aber verwirrend).

if (a > b) {
    return 0 
}

Warum der Test a>b? Und warumreturn 0

Lassen Sie uns den Code ändern, um genauer zu reflektieren, was ein Mensch tut

func sumInts(a: Int, b: Int) -> Int {
  if (a == b) {
    return b // When 'a equals b' I'm at the most Right integer, return it
  }
  else {
    return a + sumInts(a: a + 1, b: b)
  }
}

Können wir es noch menschlicher machen? Ja! Normalerweise summieren wir von links nach rechts (2 + 3 + ...). Die obige Rekursion summiert sich jedoch von rechts nach links (... + 4 + 5). Ändern Sie den Code, um ihn wiederzugeben (Das -kann ein wenig einschüchternd sein, aber nicht viel)

func sumInts(a: Int, b: Int) -> Int {
  if (a == b) {
    return b // When I'm at the most Left integer, return it
  }
  else {
    return sumInts(a: a, b: b - 1) + b
  }
}

Einige mögen diese Funktion verwirrender finden, da wir am "fernen" Ende beginnen, aber das Üben kann dazu führen, dass sie sich natürlich anfühlt (und es ist eine weitere gute "Denk" -Technik: Versuchen Sie "beide" Seiten, wenn Sie eine Rekursion lösen). Und wieder spiegelt die Funktion wider, was ein Mensch (am meisten?) Tut: Nimmt die Summe aller linken Ganzzahlen und addiert die 'nächste' rechte Ganzzahl.

user6085241
quelle
2

Es fiel mir schwer, die Rekursion zu verstehen, dann fand ich diesen Blog und ich sah diese Frage bereits, also dachte ich, ich muss sie teilen. Sie müssen diesen Blog lesen. Ich fand das äußerst hilfreich. Es erklärt mit Stack und erklärt sogar, wie zwei Rekursionen mit Stack Schritt für Schritt funktionieren. Ich empfehle Ihnen, zuerst zu verstehen, wie der Stapel funktioniert, was hier sehr gut erklärt wird: Reise zum Stapel

then now you will understand how recursion works now take a look of this post: Rekursion Schritt für Schritt verstehen

Geben Sie hier die Bildbeschreibung ein

Es ist ein Programm:

def hello(x):
    if x==1:
        return "op"
    else:
        u=1
        e=12
        s=hello(x-1)
        e+=1
        print(s)
        print(x)
        u+=1
    return e

hello(3)

Geben Sie hier die Bildbeschreibung ein Geben Sie hier die Bildbeschreibung ein


quelle
2

Rekursion machte für mich Sinn, als ich aufhörte zu lesen, was andere darüber sagen, oder es als etwas betrachtete, das ich vermeiden kann, und einfach Code schrieb. Ich habe ein Problem mit einer Lösung gefunden und versucht, die Lösung zu duplizieren, ohne zu suchen. Ich habe mir die Lösung erst angesehen, als ich hilflos feststeckte. Dann habe ich wieder versucht, es zu duplizieren. Ich habe dies bei mehreren Problemen erneut durchgeführt, bis ich mein eigenes Verständnis und Gespür dafür entwickelt habe, wie man ein rekursives Problem identifiziert und löst. Als ich dieses Level erreicht hatte, fing ich an, Probleme zu erfinden und zu lösen. Das hat mir mehr geholfen. Manchmal können Dinge nur gelernt werden, indem man sie selbst ausprobiert und kämpft. bis Sie es "bekommen".

Phil
quelle
0

Lassen Sie mich anhand eines Beispiels der Fibonacci-Serie sagen, Fibonacci ist

t (n) = t (n - 1) + n;

wenn n = 0, dann 1

so lassen Sie sehen , wie Rekursion funktioniert, ersetze ich nur nin t(n)mit n-1und so weiter. es sieht aus:

t (n - 1) = t (n - 2) + n + 1;

t (n - 1) = t (n - 3) + n + 1 + n;

t (n - 1) = t (n - 4) + n + 1 + n + 2 + n;

.

.

.

t (n) = t (nk) + ... + (nk-3) + (nk-2) + (nk-1) + n;

wir wissen , ob t(0)=(n-k)Gleichen 1dann n-k=0so n=kersetzen wir kmit n:

t (n) = t (nn) + ... + (n-n + 3) + (n-n + 2) + (n-n + 1) + n;

wenn wir n-ndann weglassen :

t (n) = t (0) + ... + 3 + 2 + 1 + (n-1) + n;

so 3+2+1+(n-1)+nist die natürliche Zahl. es berechnet alsΣ3+2+1+(n-1)+n = n(n+1)/2 => n²+n/2

das ergebnis für fib ist: O(1 + n²) = O(n²)

Dies ist der beste Weg, um die rekursive Beziehung zu verstehen

Alireza Rahmani Khalili
quelle