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 ohne Blumen.
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 wiederholt sich. Verallgemeinern wir es:
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 aus Knoten , wobei jeder Knoten eine bestimmte Anzahl von untergeordneten Knoten hat, die auch Knoten sind, oder null. Ein Binärbaum ist ein Baum aus Knoten mit genau zwei untergeordneten Elementen, die normalerweise als "links" und "rechts" bezeichnet werden. Wieder können die Kinder Knoten oder Null sein. Eine Wurzel ist ein Knoten, der keinem anderen Knoten untergeordnet 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 Knoten zu summieren, addieren wir den Wert des Knotens selbst zum Wert seines linken Kindes, falls vorhanden, und zum Wert seines rechten Kindes, falls vorhanden. Denken Sie nun daran, dass die untergeordneten Elemente, 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, addieren wir den Wert des Kinderknotens selbst zum Wert des linken Kindes, falls vorhanden, und zum Wert des rechten Kindes, falls vorhanden.
Vielleicht haben Sie vorausgesehen, wohin ich damit gehe, und möchten einen Code sehen? OK:
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 anstatt die untergeordneten Elemente explizit zu testen, um festzustellen, ob sie Null oder Knoten sind, die rekursive Funktion für einen Nullknoten nur Null zurückgibt.
Angenommen, wir haben einen Baum, der so aussieht (die Zahlen sind Werte, die Schrägstriche zeigen auf untergeordnete Elemente und @ bedeutet, dass der Zeiger auf null zeigt):
5
/ \
4 3
/\ /\
2 1 @ @
/\ /\
@@ @@
Wenn wir sumNode im Stammverzeichnis (dem Knoten mit dem Wert 5) aufrufen, 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. Überall, wo wir sumNode sehen, werden wir es durch die Erweiterung der return-Anweisung ersetzen:
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 Vorlage betrachteten? Jedes Mal, wenn wir unsere sumNode-Funktion verwendeten, behandelten wir nur einen einzelnen Knoten mit einem einzelnen if / then-Zweig und zwei einfachen return-Anweisungen, die sich beinahe selbst geschrieben hätten, direkt aus unserer Spezifikation?
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 Kraft der Rekursion.
Das obige Vasenbeispiel ist ein Beispiel für die Schwanzrekursion . Alles, was Schwanzrekursion bedeutet, ist, dass in der rekursiven Funktion, wenn wir rekursiv waren (dh wenn wir die Funktion erneut aufriefen), dies das letzte war, was wir getan haben.
Das Baumbeispiel war nicht schwanzrekursiv, denn obwohl wir als letztes das rechte Kind rekursiv gemacht haben, haben wir vorher das linke Kind rekursiv gemacht.
Tatsächlich spielte die Reihenfolge, in der wir die Kinder aufgerufen und den Wert des aktuellen Knotens addiert haben, keine Rolle, da die Addition kommutativ ist.
Schauen wir uns nun eine Operation an, bei der die Reihenfolge eine Rolle spielt. Wir werden einen binären Baum von Knoten verwenden, aber dieses Mal ist der gehaltene Wert ein Zeichen, keine Zahl.
Unser Baum hat eine spezielle Eigenschaft, dass für jeden Knoten sein Zeichen nach (in alphabetischer Reihenfolge) dem Zeichen folgt, das von seinem linken Kind und davor gehalten wird (in alphabetischer Reihenfolge) dem Zeichen seines rechten Kindes steht.
Wir möchten den Baum in alphabetischer Reihenfolge drucken. Das ist angesichts der besonderen Eigenschaft des Baumes einfach. Wir drucken nur das linke Kind, dann das Zeichen des Knotens und dann das rechte Kind.
Wir wollen nicht nur wohl oder übel drucken, sondern geben unserer Funktion etwas zum Drucken. Dies ist ein Objekt mit einer Druckfunktion (char). Wir müssen uns keine Gedanken darüber machen, wie es funktioniert. Nur 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 zur jetzt wichtigen Reihenfolge der Operationen zeigt dieses Beispiel, dass wir Dinge in eine rekursive Funktion übergeben können. Das einzige, was wir tun müssen, ist sicherzustellen, dass wir es bei jedem rekursiven Aufruf weitergeben. 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 @ @
/\ /\
@@ i@
/\
@@
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 wir gedruckt haben:
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, wir haben "ahijkn" gedruckt, was in der Tat in alphabetischer Reihenfolge ist.
Wir schaffen es, einen ganzen Baum in alphabetischer Reihenfolge zu drucken, indem wir wissen, wie man einen einzelnen Knoten in alphabetischer Reihenfolge druckt. Das war nur (weil unser Baum die besondere Eigenschaft hatte, Werte links von alphabetisch späteren Werten zu ordnen) zu wissen, dass das linke Kind vor dem Drucken des Knotenwerts und das rechte Kind nach dem Drucken des Knotenwerts gedruckt werden musste.
Und das ist die Kraft der Rekursion: in der Lage zu sein, ganze Dinge zu tun, indem man nur weiß, wie man einen Teil des Ganzen macht (und weiß, wann man aufhört zu rekursieren).
Daran erinnern, dass in den meisten Sprachen 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! Aber tatsächlich, weil ich diese Antwort mehr als vier Mal bearbeitet habe (um das letzte Beispiel hinzuzufügen, aber hauptsächlich, um Tippfehler zu korrigieren und zu polieren - das Tippen auf einer winzigen Netbook-Tastatur ist schwierig), kann ich keine Punkte mehr dafür bekommen . Was mich etwas davon abhält, mich in zukünftigen Antworten so viel Mühe zu geben.
Siehe meinen Kommentar hier dazu: /programming/128434/what-are-community-wiki-posts-in-stackoverflow/718699#718699