Come svuoti un vaso contenente cinque fiori?
Risposta: se il vaso non è vuoto, tiri fuori un fiore e poi svuoti un vaso contenente quattro fiori.
Come svuoti un vaso contenente quattro fiori?
Risposta: se il vaso non è vuoto, tiri fuori un fiore e poi svuoti un vaso contenente tre fiori.
Come svuoti un vaso contenente tre fiori?
Risposta: se il vaso non è vuoto, tiri fuori un fiore e poi svuoti un vaso contenente due fiori.
Come svuoti un vaso contenente due fiori?
Risposta: se il vaso non è vuoto, tiri fuori un fiore e poi svuoti un vaso contenente un fiore.
Come svuoti un vaso contenente un fiore?
Risposta: se il vaso non è vuoto, tiri fuori un fiore e poi svuoti un vaso senza fiori.
Come svuoti un vaso senza fiori?
Risposta: se il vaso non è vuoto, tiri fuori un fiore ma il vaso è vuoto, quindi hai finito.
È ripetitivo. Generalizziamolo:
Come svuoti un vaso contenente N fiori?
Risposta: se il vaso non è vuoto, tiri fuori un fiore e poi svuoti un vaso contenente fiori N-1 .
Hmm, possiamo vederlo nel codice?
void emptyVase( int flowersInVase ) {
if( flowersInVase > 0 ) {
// take one flower and
emptyVase( flowersInVase - 1 ) ;
} else {
// the vase is empty, nothing to do
}
}
Hmm, non avremmo potuto farlo in un ciclo for?
Perché, sì, la ricorsione può essere sostituita con l'iterazione, ma spesso la ricorsione è più elegante.
Parliamo di alberi. Nell'informatica, un albero è una struttura composta da nodi , in cui ogni nodo ha un numero di figli che sono anche nodi o null. Un albero binario è un albero fatto di nodi che hanno esattamente due figli, in genere chiamati "sinistra" e "destra"; ancora una volta i bambini possono essere nodi o null. Una radice è un nodo che non è figlio di nessun altro nodo.
Immagina che un nodo, oltre ai suoi figli, abbia un valore, un numero e immagina di voler sommare tutti i valori in qualche albero.
Per sommare il valore in ogni nodo, aggiungeremmo il valore del nodo stesso al valore del suo figlio sinistro, se presente, e il valore del suo figlio destro, se presente. Ora ricorda che i bambini, se non sono nulli, sono anche nodi.
Quindi per sommare il figlio sinistro, aggiungeremmo il valore del nodo figlio stesso al valore del figlio sinistro, se presente, e il valore del figlio destro, se presente.
Quindi per sommare il valore del figlio sinistro del figlio sinistro, aggiungeremo il valore del nodo figlio stesso al valore del figlio sinistro, se presente, e il valore del figlio destro, se presente.
Forse hai previsto dove sto andando con questo e vorresti vedere un po 'di codice? 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 ) ;
}
}
Si noti che invece di testare esplicitamente i bambini per vedere se sono null o nodi, facciamo semplicemente in modo che la funzione ricorsiva restituisca zero per un nodo null.
Quindi diciamo che abbiamo un albero che assomiglia a questo (i numeri sono valori, le barre indicano i bambini e @ indica che il puntatore punta a null):
5
/ \
4 3
/\ /\
2 1 @ @
/\ /\
@@ @@
Se chiamiamo sumNode sulla radice (il nodo con valore 5), restituiremo:
return root->value + sumNode( root->left ) + sumNode( root->right ) ;
return 5 + sumNode( node-with-value-4 ) + sumNode( node-with-value-3 ) ;
Espandiamolo sul posto. Ovunque vediamo sumNode, lo sostituiremo con l'espansione dell'istruzione return:
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 ;
Ora vedi come abbiamo conquistato una struttura di profondità e "ramificazione" arbitrarie, considerandola l'applicazione ripetuta di un modello composito? ogni volta attraverso la nostra funzione sumNode, ci siamo occupati di un solo nodo, usando un singolo ramo if / then e due semplici dichiarazioni di ritorno che quasi si sono scritte da sole, direttamente dalle nostre specifiche?
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
Questo è il potere della ricorsione.
L'esempio di vaso sopra è un esempio di ricorsione della coda . Tutto ciò che la ricorsione della coda significa è che nella funzione ricorsiva, se ricorrevamo (cioè, se avessimo chiamato di nuovo la funzione), quella era l'ultima cosa che facevamo.
L'esempio dell'albero non è ricorsivo alla coda, perché anche se l'ultima cosa che abbiamo fatto è stata quella di reclutare il bambino giusto, prima di farlo abbiamo ricorsato il bambino sinistro.
In effetti, l'ordine in cui abbiamo chiamato i figli e aggiunto il valore del nodo corrente non ha alcuna importanza, perché l'aggiunta è commutativa.
Ora diamo un'occhiata a un'operazione in cui l'ordine conta. Useremo un albero binario di nodi, ma questa volta il valore mantenuto sarà un carattere, non un numero.
Il nostro albero avrà una proprietà speciale, che per ogni nodo, il suo carattere viene dopo (in ordine alfabetico) il personaggio tenuto dal figlio sinistro e prima (in ordine alfabetico) il personaggio tenuto dal figlio destro.
Quello che vogliamo fare è stampare l'albero in ordine alfabetico. È facile da fare, data la proprietà speciale dell'albero. Stampiamo solo il figlio sinistro, quindi il carattere del nodo, quindi il figlio destro.
Non vogliamo solo stampare volenti o nolenti, quindi passeremo alla nostra funzione qualcosa su cui stampare. Questo sarà un oggetto con una funzione di stampa (carattere); non dobbiamo preoccuparci di come funziona, solo che quando viene chiamata la stampa, stamperà qualcosa, da qualche parte.
Vediamo che nel codice:
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 );
Oltre all'ordine delle operazioni che contano, questo esempio mostra che possiamo trasferire le cose in una funzione ricorsiva. L'unica cosa che dobbiamo fare è assicurarci che ad ogni chiamata ricorsiva, continuiamo a passarla. Abbiamo passato un puntatore a nodo e una stampante alla funzione e ad ogni chiamata ricorsiva li abbiamo passati "in basso".
Ora se il nostro albero appare così:
k
/ \
h n
/\ /\
a j @ @
/\ /\
@@ i@
/\
@@
Cosa stamperemo?
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
Quindi, se guardiamo solo le linee in cui siamo stati stampati:
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
Vediamo che abbiamo stampato "ahijkn", che è davvero in ordine alfabetico.
Riusciamo a stampare un intero albero, in ordine alfabetico, solo sapendo come stampare un singolo nodo in ordine alfabetico. Il che era giusto (perché il nostro albero aveva la speciale proprietà di ordinare i valori alla sinistra dei valori alfabeticamente successivi) sapendo di stampare il figlio sinistro prima di stampare il valore del nodo e di stampare il figlio giusto dopo aver stampato il valore del nodo.
E questo è il potere della ricorsione: essere in grado di fare cose intere sapendo solo come fare una parte del tutto (e sapere quando smettere di ricorrere).
Ricordando che nella maggior parte delle lingue, operatore || ("o") cortocircuiti quando il suo primo operando è vero, la funzione ricorsiva generale è:
void recurse() { doWeStop() || recurse(); }
Luc M commenta:
SO dovrebbe creare un badge per questo tipo di risposta. Congratulazioni!
Grazie Luc! Ma, in realtà, poiché ho modificato questa risposta più di quattro volte (per aggiungere l'ultimo esempio, ma principalmente per correggere errori di battitura e lucidarlo - digitando su una minuscola tastiera per netbook è difficile), non riesco a ottenere più punti per questo . Il che mi scoraggia in qualche modo dallo sforzo maggiore nelle risposte future.
Vedi il mio commento qui su questo: /programming/128434/what-are-community-wiki-posts-in-stackoverflow/718699#718699