Comment vider un vase contenant cinq fleurs?
Réponse: si le vase n'est pas vide, vous enlevez une fleur puis vous videz un vase contenant quatre fleurs.
Comment vider un vase contenant quatre fleurs?
Réponse: si le vase n'est pas vide, vous enlevez une fleur puis vous videz un vase contenant trois fleurs.
Comment vider un vase contenant trois fleurs?
Réponse: si le vase n'est pas vide, vous enlevez une fleur puis vous videz un vase contenant deux fleurs.
Comment vider un vase contenant deux fleurs?
Réponse: si le vase n'est pas vide, vous enlevez une fleur puis vous videz un vase contenant une fleur.
Comment vider un vase contenant une fleur?
Réponse: si le vase n'est pas vide, vous enlevez une fleur puis vous videz un vase ne contenant aucune fleur.
Comment vider un vase sans fleurs?
Réponse: si le vase n'est pas vide, vous enlevez une fleur mais le vase est vide donc vous avez terminé.
C'est répétitif. Généralisons-le:
Comment vider un vase contenant N fleurs?
Réponse: si le vase n'est pas vide, vous enlevez une fleur puis vous videz un vase contenant N-1 fleurs.
Hmm, pouvons-nous voir cela dans le code?
void emptyVase( int flowersInVase ) {
if( flowersInVase > 0 ) {
// take one flower and
emptyVase( flowersInVase - 1 ) ;
} else {
// the vase is empty, nothing to do
}
}
Hmm, ne pourrions-nous pas simplement faire ça dans une boucle for?
Pourquoi, oui, la récursivité peut être remplacée par l'itération, mais souvent la récursivité est plus élégante.
Parlons des arbres. En informatique, un arbre est une structure composée de nœuds , où chaque nœud a un certain nombre d'enfants qui sont également des nœuds, ou nuls. Un arbre binaire est un arbre composé de nœuds qui ont exactement deux enfants, généralement appelés "gauche" et "droite"; encore une fois, les enfants peuvent être des nœuds ou nuls. Une racine est un nœud qui n'est l'enfant d'aucun autre nœud.
Imaginez qu'un nœud, en plus de ses enfants, ait une valeur, un nombre, et imaginez que nous souhaitons additionner toutes les valeurs dans un arbre.
Pour additionner la valeur dans n'importe quel nœud, nous ajouterions la valeur du nœud lui-même à la valeur de son enfant gauche, le cas échéant, et la valeur de son enfant droit, le cas échéant. Rappelons maintenant que les enfants, s'ils ne sont pas nuls, sont également des nœuds.
Donc, pour additionner l'enfant gauche, nous ajouterions la valeur du nœud enfant lui-même à la valeur de son enfant gauche, le cas échéant, et la valeur de son enfant droit, le cas échéant.
Donc, pour additionner la valeur de l'enfant gauche de l'enfant gauche, nous ajouterions la valeur du nœud enfant lui-même à la valeur de son enfant gauche, le cas échéant, et la valeur de son enfant droit, le cas échéant.
Peut-être que vous avez anticipé où je vais avec cela et que vous aimeriez voir du code? D'ACCORD:
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 ) ;
}
}
Notez qu'au lieu de tester explicitement les enfants pour voir s'ils sont nuls ou des nœuds, nous faisons simplement que la fonction récursive retourne zéro pour un nœud nul.
Supposons donc que nous ayons un arbre qui ressemble à ceci (les nombres sont des valeurs, les barres obliques pointent vers les enfants et @ signifie que le pointeur pointe vers null):
5
/ \
4 3
/\ /\
2 1 @ @
/\ /\
@@ @@
Si nous appelons sumNode à la racine (le nœud avec la valeur 5), nous retournerons:
return root->value + sumNode( root->left ) + sumNode( root->right ) ;
return 5 + sumNode( node-with-value-4 ) + sumNode( node-with-value-3 ) ;
Développons cela en place. Partout où nous verrons sumNode, nous le remplacerons par l'extension de l'instruction 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 ;
Voyons maintenant comment nous avons conquis une structure de profondeur arbitraire et de "branchie", en la considérant comme l'application répétée d'un modèle composite? à chaque fois par le biais de notre fonction sumNode, nous avons traité un seul nœud, en utilisant une seule branche if / then, et deux instructions de retour simples qui ont presque écrit elles-mêmes, directement à partir de notre spécification?
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
Voilà le pouvoir de la récursivité.
L'exemple de vase ci-dessus est un exemple de récursivité de la queue . Tout ce que signifie la récursion de queue , c'est que dans la fonction récursive, si nous récursions (c'est-à-dire, si nous appelions à nouveau la fonction), c'était la dernière chose que nous faisions.
L'exemple d'arbre n'était pas récursif à la queue, car même si cette dernière chose que nous avons faite était de soigner l'enfant droit, avant de le faire, nous avons récursé l'enfant gauche.
En fait, l'ordre dans lequel nous avons appelé les enfants et ajouté la valeur du nœud actuel n'a pas d'importance du tout, car l'addition est commutative.
Voyons maintenant une opération où l'ordre est important. Nous utiliserons un arbre binaire de nœuds, mais cette fois la valeur retenue sera un caractère, pas un nombre.
Notre arbre aura une propriété spéciale, pour tout nœud, son caractère vient après (par ordre alphabétique) le caractère détenu par son enfant gauche et avant (par ordre alphabétique) le caractère détenu par son enfant droit.
Ce que nous voulons faire, c'est imprimer l'arbre dans l'ordre alphabétique. C'est facile à faire, étant donné la propriété spéciale de l'arbre. Nous imprimons simplement l'enfant gauche, puis le caractère du nœud, puis l'enfant droit.
Nous ne voulons pas simplement imprimer bon gré mal gré, nous allons donc passer notre fonction quelque chose à imprimer. Ce sera un objet avec une fonction print (char); nous n'avons pas à nous soucier de son fonctionnement, juste que lorsque print est appelé, il imprime quelque chose, quelque part.
Voyons cela dans le code:
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 );
En plus de l'ordre des opérations qui importe maintenant, cet exemple illustre que nous pouvons passer des choses dans une fonction récursive. La seule chose que nous devons faire est de nous assurer qu'à chaque appel récursif, nous continuons à le transmettre. Nous avons passé un pointeur de noeud et une imprimante à la fonction, et à chaque appel récursif, nous les avons passés "vers le bas".
Maintenant, si notre arbre ressemble à ceci:
k
/ \
h n
/\ /\
a j @ @
/\ /\
@@ i@
/\
@@
Qu'imprimerons-nous?
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
Donc, si nous regardons simplement les lignes, nous avons imprimé:
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
Nous voyons que nous avons imprimé "ahijkn", qui est en effet par ordre alphabétique.
Nous parvenons à imprimer un arbre entier, par ordre alphabétique, simplement en sachant imprimer un seul nœud par ordre alphabétique. Ce qui était juste (parce que notre arbre avait la propriété spéciale de classer les valeurs à gauche des valeurs alphabétiques ultérieures) de savoir imprimer l'enfant gauche avant d'imprimer la valeur du nœud et d'imprimer l'enfant droit après avoir imprimé la valeur du nœud.
Et c'est le pouvoir de la récursivité: être capable de faire des choses entières en ne sachant que comment faire une partie de l'ensemble (et en sachant quand arrêter de récidiver).
Rappelant que dans la plupart des langues, l'opérateur || ("ou") court-circuits lorsque son premier opérande est vrai, la fonction récursive générale est:
void recurse() { doWeStop() || recurse(); }
Luc M commente:
SO doit donc créer un badge pour ce type de réponse. Toutes nos félicitations!
Merci, Luc! Mais, en fait, parce que j'ai édité cette réponse plus de quatre fois (pour ajouter le dernier exemple, mais surtout pour corriger les fautes de frappe et les peaufiner - taper sur un minuscule clavier de netbook est difficile), je ne peux pas obtenir plus de points pour cela . Ce qui me décourage quelque peu de consacrer autant d'efforts aux réponses futures.
Voir mon commentaire ici à ce sujet: /programming/128434/what-are-community-wiki-posts-in-stackoverflow/718699#718699