web-dev-qa-db-de.com

Rekursion verstehen

Ich habe große Probleme mit dem Verständnis Rekursion in der Schule. Immer wenn der Professor darüber spricht, verstehe ich es, aber sobald ich es alleine probiere, geht es mir durch den Kopf.

Ich habe versucht, Türme von Hanoi die ganze Nacht zu lösen und mich völlig umgehauen. Mein Lehrbuch hat nur etwa 30 Seiten in Rekursion, daher ist es nicht allzu nützlich. Kennt jemand Bücher oder Ressourcen, die zur Klärung dieses Themas beitragen können?

216
Confused

Wie entleert man eine Vase mit fünf Blumen?

Antwort: Wenn die Vase nicht leer ist, nehmen Sie eine Blume heraus und leeren dann eine Vase mit vier Blumen.

Wie entleert man eine Vase mit vier Blumen?

Antwort: Wenn die Vase nicht leer ist, nehmen Sie eine Blume heraus und leeren dann eine Vase mit drei Blumen.

Wie entleert man eine Vase mit drei Blumen?

Antwort: Wenn die Vase nicht leer ist, nehmen Sie eine Blume heraus und leeren dann eine Vase mit zwei Blumen.

Wie entleert man eine Vase mit zwei Blumen?

Antwort: Wenn die Vase nicht leer ist, nehmen Sie eine Blume heraus und leeren dann eine Vase mit einer Blume.

Wie entleert man eine Vase mit einer Blume?

Antwort: Wenn die Vase nicht leer ist, nehmen Sie eine Blume heraus und leeren dann eine Vase, die keine Blumen enthält.

Wie entleert man eine Vase ohne Blumen?

Antwort: Wenn die Vase nicht leer ist, nehmen Sie eine Blume heraus, aber die Vase ist leer, damit Sie fertig sind.

Das ist repetitiv. Lassen Sie es uns verallgemeinern:

Wie entleert man eine Vase mit [~ # ~] n [~ # ~] Blumen?

Antwort: Wenn die Vase nicht leer ist, nehmen Sie eine Blume heraus und leeren dann eine Vase mit N-1 Blumen.

Hmm, können wir das im Code sehen?

void emptyVase( int flowersInVase ) {
  if( flowersInVase > 0 ) {
   // take one flower and
    emptyVase( flowersInVase - 1 ) ;

  } else {
   // the vase is empty, nothing to do
  }
}

Hmm, hätten wir das nicht einfach in einer for-Schleife machen können?

Warum, ja, Rekursion kann durch Iteration ersetzt werden, aber Rekursion ist oft eleganter.

Reden wir über Bäume. In der Informatik ist ein Baum eine Struktur, die aus Knoten besteht, wobei jeder Knoten eine Anzahl von Kindern hat, die auch Knoten sind, oder null. Ein binärer Baum ist ein Baum, der aus Knoten besteht, die genau zwei Kinder haben, die typischerweise als "links" und "rechts" bezeichnet werden. Auch hier können die Kinder Knoten oder Null sein. Ein root ist ein Knoten, der kein Kind eines anderen Knotens ist.

Stellen Sie sich vor, ein Knoten hat zusätzlich zu seinen untergeordneten Knoten einen Wert, eine Zahl, und stellen Sie sich vor, wir möchten alle Werte in einem Baum summieren.

Um den Wert in einem beliebigen Knoten zu summieren, addieren wir den Wert des Knotens selbst zum Wert seines linken untergeordneten Elements (falls vorhanden) und zum Wert seines rechten untergeordneten Elements (falls vorhanden). Denken Sie jetzt daran, dass die Kinder, wenn sie nicht null sind, auch Knoten sind.

Um das linke Kind zu summieren, würden wir den Wert des Kindknotens selbst zum Wert seines linken Kindes (falls vorhanden) und zum Wert seines rechten Kindes (falls vorhanden) addieren.

Um den Wert des linken Kindes des linken Kindes zu summieren, würden wir den Wert des Kindknotens selbst zum Wert des linken Kindes (falls vorhanden) und den Wert des rechten Kindes (falls vorhanden) addieren.

Vielleicht haben Sie damit gerechnet, wohin ich gehe, und möchten Code sehen? OKAY:

struct node {
  node* left;
  node* right;
  int value;
} ;

int sumNode( node* root ) {
  // if there is no tree, its sum is zero
  if( root == null ) {
    return 0 ;

  } else { // there is a tree
    return root->value + sumNode( root->left ) + sumNode( root->right ) ;
  }
}

Beachten Sie, dass die rekursive Funktion für einen Nullknoten nur Null zurückgibt, anstatt die untergeordneten Knoten explizit zu testen, um festzustellen, ob sie Null oder Knoten sind.

Nehmen wir also an, wir haben einen Baum, der so aussieht (die Zahlen sind Werte, die Schrägstriche zeigen auf Kinder und @ bedeutet, der Zeiger zeigt auf Null):

     5
    / \
   4   3
  /\   /\
 2  1 @  @
/\  /\
@@  @@

Wenn wir sumNode im Root aufrufen (den Knoten mit dem Wert 5), geben wir Folgendes zurück:

return root->value + sumNode( root->left ) + sumNode( root->right ) ;
return 5 + sumNode( node-with-value-4 ) + sumNode( node-with-value-3 ) ;

Lassen Sie uns das an Ort und Stelle erweitern. Wo immer wir sumNode sehen, ersetzen wir es durch die Erweiterung der return-Anweisung:

sumNode( node-with-value-5);
return root->value + sumNode( root->left ) + sumNode( root->right ) ;
return 5 + sumNode( node-with-value-4 ) + sumNode( node-with-value-3 ) ;

return 5 + 4 + sumNode( node-with-value-2 ) + sumNode( node-with-value-1 ) 
 + sumNode( node-with-value-3 ) ;  

return 5 + 4 
 + 2 + sumNode(null ) + sumNode( null )
 + sumNode( node-with-value-1 ) 
 + sumNode( node-with-value-3 ) ;  

return 5 + 4 
 + 2 + 0 + 0
 + sumNode( node-with-value-1 ) 
 + sumNode( node-with-value-3 ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + sumNode(null ) + sumNode( null )
 + sumNode( node-with-value-3 ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + sumNode( node-with-value-3 ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + 3 + sumNode(null ) + sumNode( null ) ; 

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + 3 + 0 + 0 ;

return 5 + 4 
 + 2 + 0 + 0
 + 1 + 0 + 0
 + 3 ;

return 5 + 4 
 + 2 + 0 + 0
 + 1 
 + 3  ;

return 5 + 4 
 + 2 
 + 1 
 + 3  ;

return 5 + 4 
 + 3
 + 3  ;

return 5 + 7
 + 3  ;

return 5 + 10 ;

return 15 ;

Sehen Sie nun, wie wir eine Struktur von beliebiger Tiefe und "Verzweigung" erobert haben, indem wir sie als wiederholte Anwendung einer zusammengesetzten Schablone betrachten? Jedes Mal, wenn wir unsere sumNode-Funktion durchlaufen haben, haben wir nur einen einzelnen Knoten mit einer einzigen if/then-Verzweigung und zwei einfachen return-Anweisungen behandelt, die sich direkt aus unserer Spezifikation heraus fast selbst geschrieben haben.

How to sum a node:
 If a node is null 
   its sum is zero
 otherwise 
   its sum is its value 
   plus the sum of its left child node
   plus the sum of its right child node

Das ist die Macht der Rekursion.


Das obige Vasenbeispiel ist ein Beispiel für Schwanzrekursion. Alles was tail recursion bedeutet, dass in der rekursiven Funktion, wenn wir rekursiv waren (dh wenn wir die Funktion erneut aufgerufen haben), das letzte war, was wir getan haben.

Das Baumbeispiel war nicht rekursiv, denn obwohl wir als letztes das rechte Kind rekursiv behandelt haben, haben wir vorher das linke Kind rekursiv behandelt.

Tatsächlich spielte die Reihenfolge, in der wir die untergeordneten Knoten aufgerufen und den aktuellen Knotenwert hinzugefügt haben, keine Rolle, da die Addition kommutativ ist.

Betrachten wir nun eine Operation, bei der es auf die Reihenfolge ankommt. Wir werden einen binären Knotenbaum verwenden, aber dieses Mal ist der festgehaltene Wert ein Zeichen, keine Zahl.

Unser Baum wird eine spezielle Eigenschaft haben, dass für jeden Knoten sein Zeichen nach (in alphabetischer Reihenfolge) das von seinem linken Kind gehaltene Zeichen und vor ( in alphabetischer Reihenfolge) das von seinem rechten Kind gehaltene Zeichen.

Wir möchten den Baum in alphabetischer Reihenfolge drucken. Das ist angesichts der besonderen Baumeigenschaft einfach zu bewerkstelligen. Wir drucken nur das linke Kind, dann das Zeichen des Knotens, dann das rechte Kind.

Wir wollen nicht nur willkürlich drucken, sondern unserer Funktion etwas zum Drucken übergeben. Dies ist ein Objekt mit einer Druckfunktion (char). Wir brauchen uns keine Gedanken darüber zu machen, wie es funktioniert. Wenn print aufgerufen wird, wird irgendwo etwas gedruckt.

Lassen Sie uns das im Code sehen:

struct node {
  node* left;
  node* right;
  char value;
} ;

// don't worry about this code
class Printer {
  private ostream& out;
  Printer( ostream& o ) :out(o) {}
  void print( char c ) { out << c; }
}

// worry about this code
int printNode( node* root, Printer& printer ) {
  // if there is no tree, do nothing
  if( root == null ) {
    return ;

  } else { // there is a tree
    printNode( root->left, printer );
    printer.print( value );
    printNode( root->right, printer );
}

Printer printer( std::cout ) ;
node* root = makeTree() ; // this function returns a tree, somehow
printNode( root, printer );

Zusätzlich zu der Reihenfolge der Operationen, die jetzt wichtig ist, zeigt dieses Beispiel, dass wir Dinge in eine rekursive Funktion überführen können. Das einzige, was wir tun müssen, ist sicherzustellen, dass wir es bei jedem rekursiven Aufruf weiterleiten. Wir haben einen Knotenzeiger und einen Drucker an die Funktion übergeben und sie bei jedem rekursiven Aufruf "down" übergeben.

Nun, wenn unser Baum so aussieht:

         k
        / \
       h   n
      /\   /\
     a  j @  @
    /\ /\
    @@ [email protected]
       /\
       @@

Was werden wir drucken?

From k, we go left to
  h, where we go left to
    a, where we go left to 
      null, where we do nothing and so
    we return to a, where we print 'a' and then go right to
      null, where we do nothing and so
    we return to a and are done, so
  we return to h, where we print 'h' and then go right to
    j, where we go left to
      i, where we go left to 
        null, where we do nothing and so
      we return to i, where we print 'i' and then go right to
        null, where we do nothing and so
      we return to i and are done, so
    we return to j, where we print 'j' and then go right to
      null, where we do nothing and so
    we return to j and are done, so
  we return to h and are done, so
we return to k, where we print 'k' and then go right to
  n where we go left to 
    null, where we do nothing and so
  we return to n, where we print 'n' and then go right to
    null, where we do nothing and so
  we return to n and are done, so 
we return to k and are done, so we return to the caller

Wenn wir uns also nur die Zeilen ansehen, die gedruckt wurden:

    we return to a, where we print 'a' and then go right to
  we return to h, where we print 'h' and then go right to
      we return to i, where we print 'i' and then go right to
    we return to j, where we print 'j' and then go right to
we return to k, where we print 'k' and then go right to
  we return to n, where we print 'n' and then go right to

Wir sehen, dass wir "ahijkn" gedruckt haben, was in der Tat in alphabetischer Reihenfolge ist.

Es gelingt uns, einen ganzen Baum in alphabetischer Reihenfolge zu drucken, indem wir nur wissen, wie ein einzelner Knoten in alphabetischer Reihenfolge gedruckt wird. Das war nur so (weil unser Baum die besondere Eigenschaft hatte, Werte links von alphabetisch späteren Werten anzuordnen), dass er das linke untergeordnete Element vor dem Drucken des Knotenwerts und das rechte untergeordnete Element nach dem Drucken des Knotenwerts drucken musste.

Und das ist die Kraft der Rekursion: in der Lage zu sein, ganze Dinge zu tun, indem man nur einen Teil des Ganzen zu tun weiß (und weiß, wann man aufhört zu rekursieren).

Unter Hinweis darauf, dass in den meisten Sprachen der Operator || ("oder") Kurzschlüsse Wenn der erste Operand wahr ist, lautet die allgemeine rekursive Funktion:

void recurse() { doWeStop() || recurse(); } 

Luc M kommentiert:

SO sollte ein Abzeichen für diese Art von Antwort erstellen. Herzliche Glückwünsche!

Danke, Luc! Da ich diese Antwort jedoch mehr als vier Mal bearbeitet habe (um das letzte Beispiel hinzuzufügen, aber hauptsächlich, um Tippfehler zu korrigieren und sie zu polieren - das Tippen auf einer winzigen Netbook-Tastatur ist schwierig), kann ich keine weiteren Punkte dafür erhalten . Was mich ein wenig davon abhält, in zukünftigen Antworten so viel Aufwand zu betreiben.

Siehe meinen Kommentar dazu hier: https://stackoverflow.com/questions/128434/what-are-community-wiki-posts-in-stackoverflow/718699#718699

581
tpdi

Dein Gehirn explodierte, weil es in eine unendliche Rekursion geriet. Das ist ein häufiger Anfängerfehler.

Ob Sie es glauben oder nicht, Sie verstehen bereits Rekursion, Sie werden nur von einer gemeinsamen, aber fehlerhaften Metapher für eine Funktion hinuntergezogen: einer kleinen Kiste mit Dingen, die rein und raus kommen.

Denken Sie an eine Aufgabe oder Prozedur wie "Weitere Informationen zur Rekursion im Netz". Das ist rekursiv und Sie haben kein Problem damit. Um diese Aufgabe abzuschließen, können Sie:

 a) Lesen Sie die Ergebnisseite von Google auf "Rekursion" 
 b) Folgen Sie nach dem Lesen dem ersten Link und ... 
 a.1) Lesen diese neue Seite über Rekursion 
 b.1) Wenn Sie es gelesen haben, folgen Sie dem ersten Link darauf und ... 
 a.2) Lesen Sie diese neue Seite über Rekursion 
 b.2) Wenn Sie es gelesen haben, folgen Sie dem ersten Link und ... 

Wie Sie sehen, haben Sie lange Zeit ohne Probleme rekursive Dinge gemacht.

Wie lange würden Sie diese Aufgabe noch erledigen? Für immer, bis dein Gehirn explodiert? Natürlich nicht, Sie werden an einem bestimmten Punkt anhalten, wenn Sie glauben, die Aufgabe erledigt zu haben.

Es ist nicht erforderlich, dies anzugeben, wenn Sie gefragt werden, ob Sie mehr über Rekursion im Netz erfahren möchten, da Sie ein Mensch sind und dies selbst ableiten können.

Computer können nicht auf Jack schließen, daher müssen Sie eine explizite Endung einfügen: "Erfahren Sie mehr über Rekursion im Netz. BIS Sie es verstehen oder maximal 10 Seiten gelesen haben".

Sie haben auch gefolgert, dass Sie auf der Google-Ergebnisseite für "Rekursion" beginnen sollten, und auch dies ist etwas, was ein Computer nicht kann. Die vollständige Beschreibung unserer rekursiven Aufgabe muss auch einen expliziten Ausgangspunkt enthalten:

"Erfahren Sie mehr über Rekursion im Netz, BIS Sie es verstehen oder maximal 10 Seiten gelesen haben und ab www.google.com/search?q=recursion"

Ich schlage vor, Sie versuchen eines der folgenden Bücher, um das Ganze zu verstehen:

  • Common LISP: Eine sanfte Einführung in die symbolische Berechnung. Dies ist die süßeste nicht-mathematische Erklärung der Rekursion.
  • Der kleine Intrigant.
35
cfischer

Um die Rekursion zu verstehen, müssen Sie nur auf dem Etikett Ihrer Shampooflasche nachsehen:

function repeat()
{
   rinse();
   lather();
   repeat();
}

Das Problem dabei ist, dass es keine Abbruchbedingung gibt und die Rekursion auf unbestimmte Zeit wiederholt wird oder bis Sie kein Shampoo oder heißes Wasser mehr haben (externe Abbruchbedingungen, ähnlich wie beim Blasen Ihres Stapels).

24
dar7yl

Wenn Sie ein Buch wollen, das Rekursion in einfachen Worten gut erklärt, schauen Sie sich Gödel, Escher, Bach: Ein ewiger goldener Zopf von Douglas Hofstadter an, insbesondere Kapitel 5. Zusätzlich zur Rekursion Es ist eine gute Aufgabe, eine Reihe komplexer Konzepte in den Bereichen Informatik und Mathematik verständlich zu erklären, wobei eine Erklärung auf einer anderen aufbaut. Wenn Sie bisher noch nicht viel mit diesen Konzepten zu tun hatten, kann dies ein umwerfendes Buch sein.

11
Chris Upchurch

Dies ist eher eine Beschwerde als eine Frage. Haben Sie eine genauere Frage zur Rekursion? Wie bei der Multiplikation wird nicht viel darüber geschrieben.

Apropos Multiplikation, denken Sie daran.

Frage:

Was ist a * b?

Antworten:

Wenn b 1 ist, ist es a. Ansonsten ist es a + a * (b-1).

Was ist ein * (b-1)? In der obigen Frage finden Sie eine Lösung.

9
S.Lott

Ich denke, diese sehr einfache Methode sollte Ihnen helfen, die Rekursion zu verstehen. Die Methode ruft sich selbst auf, bis eine bestimmte Bedingung erfüllt ist, und gibt dann Folgendes zurück:

function writeNumbers( aNumber ){
 write(aNumber);
 if( aNumber > 0 ){
  writeNumbers( aNumber - 1 );
 }
 else{
  return;
 }
}

Diese Funktion druckt alle Zahlen ab der ersten eingegebenen Zahl bis 0 aus.

writeNumbers( 10 );
//This wil write: 10 9 8 7 6 5 4 3 2 1 0
//and then stop because aNumber is no longer larger then 0

Bassisch passiert, dass writeNumbers (10) 10 schreibt und dann writeNumbers (9) aufruft, der 9 schreibt und dann writeNumber (8) usw. aufruft. Bis writeNumbers (1) 1 schreibt und dann writeNumbers (0) aufruft, der 0 schreibt butt ruft keine writeNumbers (-1) auf;

Dieser Code ist im Wesentlichen der gleiche wie:

for(i=10; i>0; i--){
 write(i);
}

Warum sollten Sie dann eine Rekursion verwenden, wenn eine for-Schleife im Wesentlichen dasselbe tut? Nun, Sie verwenden meistens die Rekursion, wenn Sie für Schleifen nisten müssten, aber nicht wissen, wie tief sie verschachtelt sind. Zum Beispiel beim Ausdrucken von Elementen aus verschachtelten Arrays:

var nestedArray = Array('Im a string', 
                        Array('Im a string nested in an array', 'me too!'),
                        'Im a string again',
                        Array('More nesting!',
                              Array('nested even more!')
                              ),
                        'Im the last string');
function printArrayItems( stringOrArray ){
 if(typeof stringOrArray === 'Array'){
   for(i=0; i<stringOrArray.length; i++){ 
     printArrayItems( stringOrArray[i] );
   }
 }
 else{
   write( stringOrArray );
 }
}

printArrayItems( stringOrArray );
//this will write:
//'Im a string' 'Im a string nested in an array' 'me too' 'Im a string again'
//'More nesting' 'Nested even more' 'Im the last string'

Diese Funktion kann ein Array enthalten, das in 100 Ebenen verschachtelt ist. Wenn Sie eine for-Schleife schreiben, müssen Sie diese 100-mal verschachteln:

for(i=0; i<nestedArray.length; i++){
 if(typeof nestedArray[i] == 'Array'){
  for(a=0; i<nestedArray[i].length; a++){
   if(typeof nestedArray[i][a] == 'Array'){
    for(b=0; b<nestedArray[i][a].length; b++){
     //This would be enough for the nestedAaray we have now, but you would have
     //to nest the for loops even more if you would nest the array another level
     write( nestedArray[i][a][b] );
    }//end for b
   }//endif typeod nestedArray[i][a] == 'Array'
   else{ write( nestedArray[i][a] ); }
  }//end for a
 }//endif typeod nestedArray[i] == 'Array'
 else{ write( nestedArray[i] ); }
}//end for i

Wie Sie sehen, ist die rekursive Methode viel besser.

9
Pim Jager

Tatsächlich verwenden Sie die Rekursion, um die Komplexität Ihres Problems zu verringern. Sie wenden eine Rekursion an, bis Sie einen einfachen Basisfall erreichen, der leicht gelöst werden kann. Damit können Sie den letzten rekursiven Schritt lösen. Und damit kommen alle anderen rekursiven Schritte zu Ihrem ursprünglichen Problem.

8
unbeknown

Rekursion

Methode A ruft Methode A auf. Letztendlich wird eine dieser Methoden A nicht aufgerufen und beendet, aber es ist eine Rekursion, weil sich etwas selbst aufruft.

Beispiel für eine Rekursion, bei der ich jeden Ordnernamen auf der Festplatte ausdrucken möchte: (in c #)

public void PrintFolderNames(DirectoryInfo directory)
{
    Console.WriteLine(directory.Name);

    DirectoryInfo[] children = directory.GetDirectories();

    foreach(var child in children)
    {
        PrintFolderNames(child); // See we call ourself here...
    }
}
5
Sekhat

Ich werde versuchen, es mit einem Beispiel zu erklären.

Weißt du was n! meint? Wenn nicht: http://en.wikipedia.org/wiki/Factorial

3! = 1 * 2 * 3 = 6

hier geht einige Pseudocode

function factorial(n) {
  if (n==0) return 1
  else return (n * factorial(n-1))
}

Also lass es uns versuchen:

factorial(3)

ist n 0?

nein!

so graben wir tiefer mit unserer Rekursion:

3 * factorial(3-1)

3-1 = 2

ist 2 == 0?

nein!

also gehen wir tiefer! 3 * 2 * Fakultät (2-1) 2-1 = 1

ist 1 == 0?

nein!

also gehen wir tiefer! 3 * 2 * 1 * Fakultät (1-1) 1-1 = 0

ist 0 == 0?

ja!

wir haben einen trivialen Fall

wir haben also 3 * 2 * 1 * 1 = 6

ich hoffe das hat dir geholfen

5
Zoran Zaric

Welches Buch benutzt du?

Das Standardlehrbuch über Algorithmen, das eigentlich gut ist, ist Cormen & Rivest. Meine Erfahrung ist, dass es die Rekursion ziemlich gut lehrt.

Rekursion ist einer der schwierigeren Teile des Programmierens, und obwohl es Instinkt erfordert, kann es gelernt werden. Aber es braucht eine gute Beschreibung, gute Beispiele und gute Illustrationen.

Außerdem sind 30 Seiten im Allgemeinen eine Menge, 30 Seiten in einer einzigen Programmiersprache sind verwirrend. Versuchen Sie nicht, die Rekursion in C oder Java zu erlernen, bevor Sie die Rekursion im Allgemeinen aus einem allgemeinen Buch verstanden haben.

4
Uri

Eine rekursive Funktion ist einfach eine Funktion, die sich so oft wie nötig selbst aufruft. Dies ist nützlich, wenn Sie etwas mehrmals verarbeiten müssen, sich jedoch nicht sicher sind, wie oft dies tatsächlich erforderlich sein wird. In gewisser Weise könnte man sich eine rekursive Funktion als eine Art Schleife vorstellen. Wie bei einer Schleife müssen Sie jedoch die Bedingungen angeben, unter denen der Prozess unterbrochen werden soll, da er sonst unendlich wird.

4
VirtuosiMedia

http://javabat.com ist ein lustiger und aufregender Ort, um Rekursion zu üben. Ihre Beispiele beginnen ziemlich leicht und arbeiten sich durch umfangreiche (wenn Sie es so weit bringen wollen). Anmerkung: Ihr Ansatz wird durch Üben erlernt. Hier ist eine rekursive Funktion, die ich geschrieben habe, um einfach eine for-Schleife zu ersetzen.

Die for-Schleife:

public printBar(length)
{
  String holder = "";
  for (int index = 0; i < length; i++)
  {
    holder += "*"
  }
  return holder;
}

Hier ist die Rekursion, dasselbe zu tun. (Beachten Sie, dass wir die erste Methode überladen, um sicherzustellen, dass sie genau wie oben verwendet wird). Wir haben auch eine andere Methode, um unseren Index zu pflegen (ähnlich wie es die for-Anweisung für Sie oben macht). Die rekursive Funktion muss einen eigenen Index pflegen.

public String printBar(int Length) // Method, to call the recursive function
{
  printBar(length, 0);
}

public String printBar(int length, int index) //Overloaded recursive method
{
  // To get a better idea of how this works without a for loop
  // you can also replace this if/else with the for loop and
  // operationally, it should do the same thing.
  if (index >= length)
    return "";
  else
    return "*" + printBar(length, index + 1); // Make recursive call
}

Um es kurz zu machen, Rekursion ist eine gute Möglichkeit, weniger Code zu schreiben. In der letzten printBar sehen Sie, dass wir eine if-Anweisung haben. Wenn unser Zustand erreicht ist, verlassen wir die Rekursion und kehren zur vorherigen Methode zurück, die zur vorherigen Methode zurückkehrt usw. Wenn ich eine printBar (8) einschicke, erhalte ich ********. Ich hoffe, dass mit einem Beispiel einer einfachen Funktion, die dasselbe wie eine for-Schleife macht, dies vielleicht helfen wird. Sie können dies jedoch unter Java Bat üben.

4
Jeff Ancel

Einfaches rekursives Beispiel in Common LISP:

MYMAP wendet eine Funktion auf jedes Element in einer Liste an.

1) eine leere Liste hat kein Element, also geben wir die leere Liste zurück ) und NIL sind beide die leere Liste.

--- (2) Wende die Funktion auf die erste Liste an, rufe MYMAP für den Rest der Liste auf (den rekursiven Aufruf) und kombiniere beide Ergebnisse zu einer neuen Liste.

(DEFUN MYMAP (FUNCTION LIST)
  (IF (NULL LIST)
      ()
      (CONS (FUNCALL FUNCTION (FIRST LIST))
            (MYMAP FUNCTION (REST LIST)))))

Sehen wir uns die verfolgte Hinrichtung an. Beim Eingeben einer Funktion werden die Argumente gedruckt. Beim Beenden einer Funktion wird das Ergebnis gedruckt. Bei jedem rekursiven Aufruf wird die Ausgabe auf Ebene eingerückt.

In diesem Beispiel wird die SIN-Funktion für jede Nummer in einer Liste aufgerufen (1 2 3 4).

Command: (mymap 'sin '(1 2 3 4))

1 Enter MYMAP SIN (1 2 3 4)
| 2 Enter MYMAP SIN (2 3 4)
|   3 Enter MYMAP SIN (3 4)
|   | 4 Enter MYMAP SIN (4)
|   |   5 Enter MYMAP SIN NIL
|   |   5 Exit MYMAP NIL
|   | 4 Exit MYMAP (-0.75680256)
|   3 Exit MYMAP (0.14112002 -0.75680256)
| 2 Exit MYMAP (0.9092975 0.14112002 -0.75680256)
1 Exit MYMAP (0.841471 0.9092975 0.14112002 -0.75680256)

Dies ist unser Ergebnis:

(0.841471 0.9092975 0.14112002 -0.75680256)
3
Rainer Joswig

Kinder verwenden implizit eine Rekursion, zum Beispiel:

Roadtrip nach Disney World

Sind wir schon da? (Nein)

Sind wir schon da? (Bald)

Sind wir schon da? (Fast ...)

Sind wir schon da? (SHHHH)

Sind wir schon da?(!!!!!)

An diesem Punkt schläft das Kind ein ...

Diese Countdown-Funktion ist ein einfaches Beispiel:

function countdown()
      {
      return (arguments[0] > 0 ?
        (
        console.log(arguments[0]),countdown(arguments[0] - 1)) : 
        "done"
        );
      }
countdown(10);

Hofstadter-Gesetz angewendet auf Softwareprojekte ist ebenfalls relevant.

Das Wesen der menschlichen Sprache ist nach Chomsky die Fähigkeit des endlichen Gehirns, das zu produzieren, was er für unendliche Grammatiken hält. Damit meint er nicht nur, dass es keine Obergrenze für das gibt, was wir sagen können, sondern dass es auch keine Obergrenze für die Anzahl der Sätze gibt, die unsere Sprache hat, und dass es keine Obergrenze für die Größe eines bestimmten Satzes gibt. Chomsky hat behauptet, dass das grundlegende Werkzeug, das all dieser Kreativität der menschlichen Sprache zugrunde liegt, die Rekursion ist: die Fähigkeit, dass eine Phrase in einer anderen Phrase desselben Typs wieder vorkommt. Wenn ich "Johns Bruderhaus" sage, habe ich ein Nomen "Haus", das in einer Nomenphrase "Bruderhaus" vorkommt, und diese Nomenphrase kommt in einer anderen Nomenphrase vor, "Johns Bruderhaus". Das macht sehr viel Sinn und ist eine interessante Eigenschaft der menschlichen Sprache.

Referenzen

3
Paul Sweatte

Um die Rekursion einem Sechsjährigen zu erklären, erklären Sie sie zuerst einem Fünfjährigen und warten Sie dann ein Jahr.

Tatsächlich ist dies ein nützliches Gegenbeispiel, da Ihr rekursiver Aufruf einfacher und nicht schwieriger sein sollte. Es wäre noch schwieriger, einem Fünfjährigen die Rekursion zu erklären, und obwohl Sie die Rekursion bei 0 stoppen könnten, haben Sie keine einfache Lösung, um die Rekursion einem Nulljährigen zu erklären.

Um ein Problem mithilfe der Rekursion zu lösen, unterteilen Sie es zunächst in eines oder mehrere einfachere Probleme, die Sie auf die gleiche Weise lösen können. Wenn das Problem dann einfach genug ist, um es ohne weitere Rekursion zu lösen, können Sie dies tun kehre zu höheren Ebenen zurück.

Tatsächlich war dies eine rekursive Definition, wie ein Problem mit der Rekursion gelöst werden kann.

3
dlaliberte

Die wirklich mathematische Methode zum Erstellen einer rekursiven Funktion lautet wie folgt:

1: Stellen Sie sich vor, Sie haben eine Funktion, die für f (n-1) korrekt ist, und bauen f so auf, dass f(n) richtig ist. 2: Bauen Sie f so auf, dass f(1) ist richtig.

Auf diese Weise können Sie mathematisch beweisen, dass die Funktion korrekt ist und den Namen Induktion hat. Es ist äquivalent, verschiedene Basisfälle oder kompliziertere Funktionen für mehrere Variablen zu haben. Es ist auch äquivalent, sich vorzustellen, dass f(x) für alle x korrekt ist

Nun zu einem "einfachen" Beispiel. Erstellen Sie eine Funktion, mit der bestimmt werden kann, ob eine Münzkombination von 5 Cent und 7 Cent möglich ist, um x Cent zu erhalten. Zum Beispiel ist es möglich, 17 Cent mal 2x5 + 1x7 zu haben, aber unmöglich, 16 Cent zu haben.

Stellen Sie sich nun vor, Sie haben eine Funktion, die Ihnen sagt, ob es möglich ist, x Cent zu erstellen, solange x <n ist. Rufen Sie diese Funktion can_create_coins_small auf. Es sollte ziemlich einfach sein, sich vorzustellen, wie man die Funktion für n erstellt. Bauen Sie nun Ihre Funktion auf:

bool can_create_coins(int n)
{
    if (n >= 7 && can_create_coins_small(n-7))
        return true;
    else if (n >= 5 && can_create_coins_small(n-5))
        return true;
    else
        return false;
}

Der Trick hier ist zu erkennen, dass die Tatsache, dass can_create_coins für n funktioniert, bedeutet, dass Sie can_create_coins durch can_create_coins_small ersetzen können.

bool can_create_coins(int n)
{
    if (n >= 7 && can_create_coins(n-7))
        return true;
    else if (n >= 5 && can_create_coins(n-5))
        return true;
    else
        return false;
}

Als letztes müssen Sie einen Basisfall haben, um die unendliche Rekursion zu stoppen. Beachten Sie: Wenn Sie versuchen, 0 Cent zu verdienen, ist dies möglich, wenn Sie keine Münzen haben. Das Hinzufügen dieser Bedingung ergibt:

bool can_create_coins(int n)
{
    if (n == 0)
        return true;
    else if (n >= 7 && can_create_coins(n-7))
        return true;
    else if (n >= 5 && can_create_coins(n-5))
        return true;
    else
        return false;
}

Mit der Methode nendlicher Abstieg kann bewiesen werden, dass diese Funktion immer zurückgegeben wird, aber das ist hier nicht erforderlich. Sie können sich vorstellen, dass f(n) nur niedrigere Werte von n aufruft und letztendlich immer 0 erreicht.

Um diese Informationen zur Lösung Ihres Tower of Hanoi-Problems zu verwenden, gehe ich davon aus, dass Sie eine Funktion haben, mit der Sie n-1 Tabletts von a nach b (für jedes a/b) bewegen und versuchen, n Tabellen von a nach b zu bewegen .

3
FryGuy

Autsch. Ich habe letztes Jahr versucht, die Türme von Hanoi herauszufinden. Das Schwierige an TOH ist, dass es sich nicht um ein einfaches Beispiel für eine Rekursion handelt. Sie haben verschachtelte Rekursionen, die auch die Rollen der Türme bei jedem Aufruf ändern. Der einzige Weg, wie ich es sinnvoll machen konnte, bestand darin, die Bewegung der Ringe in meinem geistigen Auge buchstäblich zu visualisieren und zu verbalisieren, was der rekursive Aufruf sein würde. Ich würde mit einem einzigen Klingeln beginnen, dann zwei, dann drei. Ich habe das Spiel tatsächlich im Internet bestellt. Ich brauchte vielleicht zwei oder drei Tage, um mein Gehirn zu knacken.

2
Jack BeNimble

Wenn ich mit rekursiven Lösungen arbeite, versuche ich immer:

  • Bestimmen Sie zuerst den Basisfall, d. H. Wenn n = 1 in einer Lösung für Fakultät
  • Versuchen Sie, eine allgemeine Regel für jeden anderen Fall aufzustellen

Es gibt auch verschiedene Arten von rekursiven Lösungen, es gibt den Divide and Conquer-Ansatz, der für Fraktale und viele andere nützlich ist.

Es wäre auch hilfreich, wenn Sie zunächst an einfacheren Problemen arbeiten könnten, um den Überblick zu behalten. Einige Beispiele lösen für die Fakultät und erzeugen die n-te Fibonacci-Zahl.

Für Referenzen empfehle ich Algorithmen von Robert Sedgewick.

Hoffentlich hilft das. Viel Glück.

2
Mark Basmayor

Denken Sie an eine Arbeiterbiene. Es versucht, Honig zu machen. Es macht seine Arbeit und erwartet, dass andere Arbeiter Bienen den Rest des Honigs machen. Und wenn die Wabe voll ist, hört sie auf.

Denken Sie es als Magie. Sie haben eine Funktion mit demselben Namen wie die, die Sie implementieren möchten, und wenn Sie ihr das Teilproblem geben, löst sie es für Sie. Sie müssen lediglich die Lösung Ihres Teils in die Lösung integrieren gab dir.

Zum Beispiel wollen wir die Länge einer Liste berechnen. Nennen wir unsere Funktion magical_length und unseren magischen Helfer magical_length. Wir wissen, dass, wenn wir die Unterliste angeben, die nicht das erste Element enthält, sie uns die Länge der Unterliste magisch angibt. Dann müssen wir uns nur noch überlegen, wie wir diese Informationen in unseren Job integrieren können. Die Länge des ersten Elements ist 1 und magic_counter gibt die Länge der Unterliste n-1 an, daher beträgt die Gesamtlänge (n-1) + 1 -> n

int magical_length( list )
  sublist = rest_of_the_list( list )
  sublist_length = magical_length( sublist ) // you can think this function as magical and given to you
  return 1 + sublist_length

Diese Antwort ist jedoch unvollständig, da wir nicht berücksichtigt haben, was passiert, wenn wir eine leere Liste angeben. Wir dachten, dass die Liste, die wir haben, immer mindestens ein Element hat. Daher müssen wir uns überlegen, was die Antwort sein soll, wenn wir eine leere Liste erhalten und die Antwort offensichtlich 0 ist. Fügen Sie diese Informationen also zu unserer Funktion hinzu, und dies wird als Basis-/Kantenbedingung bezeichnet.

int magical_length( list )
  if ( list is empty) then
    return 0
  else
    sublist_length = magical_length( sublist ) // you can think this function as magical and given to you
    return 1 + sublist_length
1
reader_1000

Eine rekursive Funktion ist wie eine Feder, die Sie bei jedem Aufruf ein wenig komprimieren. Bei jedem Schritt legen Sie einige Informationen (den aktuellen Kontext) auf einem Stapel ab. Wenn der letzte Schritt erreicht ist, wird die Feder freigegeben und alle Werte (Kontexte) auf einmal gesammelt!

Nicht sicher, ob diese Metapher effektiv ist ... :-)

Abgesehen von den klassischen Beispielen (Fakultät, die das schlechteste Beispiel ist, da sie ineffizient und leicht abgeflacht sind, Fibonacci, Hanoi ...), die ein bisschen künstlich sind (ich verwende sie selten, wenn überhaupt, in realen Programmierfällen), ist dies der Fall interessant zu sehen, wo es wirklich verwendet wird.

Ein sehr häufiger Fall ist das Gehen auf einem Baum (oder einer Grafik, aber Bäume sind im Allgemeinen häufiger).
Zum Beispiel eine Ordnerhierarchie: Um die Dateien aufzulisten, iterieren Sie darauf. Wenn Sie ein Unterverzeichnis finden, ruft die Funktion, die die Dateien auflistet, sich selbst mit dem neuen Ordner als Argument auf. Wenn Sie von der Auflistung dieses neuen Ordners (und seiner Unterordner!) Zurückkehren, wird der Kontext zur nächsten Datei (oder zum nächsten Ordner) fortgesetzt.
Ein weiterer konkreter Fall ist das Zeichnen einer Hierarchie von GUI-Komponenten: Es ist üblich, Container wie Fenster für Komponenten zu haben, die auch Fenster sein können, oder zusammengesetzte Komponenten usw. Die Malroutine ruft rekursiv die Funktion Malen auf jeder Komponente, die die Paint-Funktion aller darin enthaltenen Komponenten aufruft, usw.

Ich bin mir nicht sicher, ob ich sehr klar bin, aber ich zeige gerne, wie man Unterrichtsmaterial in der Praxis einsetzt, da ich in der Vergangenheit darauf gestoßen bin.

1
PhiLho