J'ai beaucoup de mal à comprendre récursion à l'école. Chaque fois que le professeur en parle, il me semble l'avoir, mais dès que je l'essaie moi-même, cela me blesse complètement la tête.
J'essayais de résoudre Tours de Hanoi toute la nuit et me suis complètement bouleversé. Mon manuel ne compte que 30 pages environ en récurrence, il n’est donc pas très utile. Est-ce que quelqu'un connaît des livres ou des ressources pouvant aider à clarifier ce sujet?
Comment vider un vase contenant cinq fleurs?
Réponse: si le vase n'est pas vide, vous en sortez une, 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 en sortez une, 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 en sortez une, 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 en sortez une, 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 pas de fleurs.
Comment vider un vase ne contenant pas de fleurs?
Réponse: si le vase n'est pas vide, vous en sortez une fleur mais le vase est vide, vous avez terminé.
C'est répétitif. Généralisons-le:
Comment vider un vase contenant des fleurs [~ # ~] n [~ # ~] ?
Réponse: si le vase n'est pas vide, vous en sortez une, 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, aurions-nous pu faire cela dans une boucle?
Pourquoi, oui, la récursion peut être remplacée par l'itération, mais souvent la récursion 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 null. Un arbre binaire est un arbre composé de nœuds ayant exactement deux enfants , généralement appelé "gauche" et "droite"; encore une fois, les enfants peuvent être des noeuds ou null Une racine est un nœud qui n'est pas l'enfant d'un autre nœud.
Imaginez qu’un nœud, en plus de ses enfants, ait une valeur, un nombre, et imaginons que nous souhaitons additionner toutes les valeurs d’un arbre.
Pour additionner la valeur d'un nœud, ajoutons la valeur du nœud lui-même à la valeur de son enfant de gauche, le cas échéant, et à la valeur de son enfant de droite, le cas échéant. Rappelons maintenant que les enfants, s'ils ne sont pas nuls, sont aussi des nœuds.
Donc, pour résumer l'enfant de gauche, nous ajouterions la valeur du noeud enfant lui-même à la valeur de son enfant de gauche, le cas échéant, et à la valeur de son enfant de droite, le cas échéant.
Donc, pour résumer la valeur de l'enfant gauche de l'enfant de gauche, nous ajouterions la valeur du noeud enfant lui-même à la valeur de son enfant de gauche, le cas échéant, et à la valeur de son enfant de droite, le cas échéant.
Vous avez peut-être prévu où je veux en venir et souhaiteriez 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 non, nous faisons simplement en sorte que la fonction récursive renvoie zéro pour un nœud nul.
Disons donc que nous avons un arbre qui ressemble à ceci (les nombres sont des valeurs, les barres obliques désignent des enfants et @ signifie que le pointeur indique null):
5
/ \
4 3
/\ /\
2 1 @ @
/\ /\
@@ @@
Si nous appelons sumNode à la racine (le nœud de 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 voyons 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 ;
Maintenant, voyez comment nous avons conquis une structure de profondeur et de "branchement" arbitraires, en la considérant comme l'application répétée d'un modèle composite. chaque fois, via 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 été écrites, 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
C'est le pouvoir de la récursivité.
L'exemple de vase ci-dessus est un exemple de récursion de la queue . Tout ce que cela signifie récursivité , c’est que dans la fonction récursive, si nous sommes récursifs (c’est-à-dire si nous appelons à nouveau la fonction), c’était la dernière chose à faire. Nous faisions.
L'exemple de l'arbre n'était pas récursif, car même si la dernière chose à faire était de récidiver le bon enfant, avant nous, nous avions récursif le gauche.
En fait, l'ordre dans lequel nous avons appelé les enfants et ajouté la valeur du nœud actuel importait peu, car l'addition est commutative.
Examinons maintenant une opération où l'ordre est important. Nous utiliserons une arborescence binaire de nœuds, mais cette fois la valeur conservée sera un caractère, pas un nombre.
Notre arbre aura une propriété spéciale, que 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 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 de gauche, puis le personnage du nœud, puis l'enfant de droite.
Nous ne voulons pas seulement imprimer bon gré mal gré, nous allons donc transmettre à notre fonction un élément à imprimer. Ce sera un objet avec une fonction print (char); nous n'avons pas besoin de nous soucier de la façon dont cela fonctionne, mais simplement que lorsque vous appelez print, vous imprimez 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 compte maintenant, cet exemple montre que nous pouvons passer des choses à une fonction récursive. La seule chose que nous devons faire est de veiller à ce que, à chaque appel récursif, nous continuons à le transmettre. Nous avons passé un pointeur de nœud 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'allons-nous imprimer?
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 ne regardons que les lignes où 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 dans l'ordre alphabétique.
Nous parvenons à imprimer un arbre complet, dans l’ordre alphabétique, simplement en sachant imprimer un seul nœud dans l’ordre alphabétique. Ce qui était juste (parce que notre arbre avait la propriété spéciale de classer les valeurs à gauche des valeurs ultérieures alphabétiquement) en sachant d’imprimer le fils gauche avant d’imprimer la valeur du nœud et d’imprimer le bon enfant 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 sachant seulement comment faire une partie du tout (et en sachant quand arrêter de récurer) .
Rappelant que dans la plupart des langues, l'opérateur || ("ou") court-circuite quand son premier opérande est vrai, la fonction récursive générale est:
void recurse() { doWeStop() || recurse(); }
Luc M commente:
SO devrait donc créer un badge pour ce genre de réponse. Toutes nos félicitations!
Merci Luc! Mais en réalité, parce que j'ai modifié cette réponse plus de quatre fois (pour ajouter le dernier exemple, mais surtout pour corriger les fautes de frappe et la 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 un peu de mettre autant d’effort dans les réponses futures.
Voir mon commentaire ici à ce sujet: https://stackoverflow.com/questions/128434/what-are-community-wiki-posts-in-stackoverflow/718699#718699
Votre cerveau a explosé parce qu'il entrait dans une récursion infinie. C'est une erreur commune au débutant.
Croyez-le ou non, vous comprenez déjà la récursivité, vous êtes simplement entraîné par une métaphore commune, mais erronée, pour une fonction: une petite boîte avec des éléments entrants et sortants.
Pensez à la place d'une tâche ou d'une procédure, telle que "En savoir plus sur la récursion sur le net". C'est récursif et cela ne vous pose aucun problème. Pour compléter cette tâche, vous pouvez:
a) Lisez la page de résultats de Google pour "récursivité" b) Une fois que vous l'avez lue, suivez le premier lien dessus et ... a.1) Lisez cette nouvelle page sur la récursivité b.1) Une fois que vous l'avez lue, suivez le premier lien qui s'y trouve et ... a.2) Lisez cette nouvelle page sur la récursivité b.2) Une fois que vous l'avez lu, suivez le premier lien dessus et ...
Comme vous pouvez le constater, vous faites du contenu récursif depuis longtemps sans aucun problème.
Pendant combien de temps continueriez-vous à faire cette tâche? Pour toujours jusqu'à ce que votre cerveau explose? Bien sûr que non, vous vous arrêterez à un moment donné, chaque fois que vous penserez avoir terminé la tâche.
Inutile de préciser cela lorsque vous êtes invité à "en savoir plus sur la récursion sur le net", car vous êtes un humain et vous pouvez en déduire vous-même.
L'ordinateur ne peut pas déduire la prise, vous devez donc inclure une fin explicite: "pour en savoir plus sur la récursion sur le net, JUSQU'À ce que vous compreniez ou que vous ayez lu un maximum de 10 pages".
Vous en avez également déduit que vous devriez commencer par la "récursivité" sur la page de résultats de Google. Encore une fois, un ordinateur ne peut pas le faire. La description complète de notre tâche récursive doit également inclure un point de départ explicite:
"en savoir plus sur la récursion sur le net, JUSQU'À ce que vous compreniez ou que vous ayez lu un maximum de 10 pages et à partir de www.google.com/search?q=recursion"
Pour tout comprendre, je vous suggère d'essayer l'un de ces livres:
Pour comprendre la récursivité, il suffit de regarder l'étiquette de votre bouteille de shampoing:
function repeat()
{
rinse();
lather();
repeat();
}
Le problème, c’est qu’il n’ya pas de condition de terminaison et que la récursivité se répète indéfiniment, ou jusqu’à épuisement du shampoing ou de l’eau chaude (conditions de terminaison externes similaires à celles du soufflage de votre pile).
Si vous voulez un livre qui explique bien la récursion en termes simples, jetez un oeil à Gödel, Escher, Bach: Une éternelle tresse d'or de Douglas Hofstadter, plus précisément au chapitre 5. En plus de la récursion Il fait un bon travail consistant à expliquer de manière compréhensible un certain nombre de concepts complexes en informatique et en mathématiques, une explication s'appuyant sur une autre. Si vous n'avez pas eu beaucoup d'expérience avec ce genre de concepts auparavant, cela peut être un livre époustouflant.
C'est plus une plainte qu'une question. Avez-vous une question plus spécifique sur la récursivité? Comme la multiplication, les gens n’écrivent pas beaucoup.
En parlant de multiplication, pensez à ceci.
Question:
Qu'est-ce qu'un * b?
Répondre:
Si b est 1, c'est a. Sinon, c'est a + a * (b-1).
Qu'est-ce qu'un * (b-1)? Voir la question ci-dessus pour savoir comment résoudre le problème.
Je pense que cette méthode très simple devrait vous aider à comprendre la récursivité. La méthode s'appellera jusqu'à ce qu'une certaine condition soit vraie et retournera:
function writeNumbers( aNumber ){
write(aNumber);
if( aNumber > 0 ){
writeNumbers( aNumber - 1 );
}
else{
return;
}
}
Cette fonction permet d’imprimer tous les nombres du premier numéro jusqu’à 0. Ainsi:
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
Ce qui se passe dans les faits, c’est que writeNumbers (10) écrit 10, puis appelle writeNumbers (9), qui écrit 9, puis writeNumber (8), etc. butt n'appellera pas writeNumbers (-1);
Ce code est essentiellement le même que:
for(i=10; i>0; i--){
write(i);
}
Alors pourquoi utiliser la récursion que vous pourriez demander, si une boucle for fait essentiellement la même chose. Eh bien, vous utilisez principalement la récursivité lorsque vous devez imbriquer des boucles sans savoir à quelle profondeur elles sont imbriquées. Par exemple, lorsque vous imprimez des éléments à partir de tableaux imbriqués:
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'
Cette fonction peut prendre un tableau qui peut être imbriqué dans 100 niveaux, alors que l'écriture d'une boucle for nécessiterait alors que vous l'imbriquiez 100 fois:
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
Comme vous pouvez le constater, la méthode récursive est bien meilleure.
En fait, vous utilisez la récursivité pour réduire la complexité de votre problème. Vous appliquez la récursivité jusqu'à atteindre un cas de base simple qui peut être résolu facilement. Avec cela, vous pouvez résoudre la dernière étape récursive. Et avec cela toutes les autres étapes récursives à votre problème d'origine.
Récursion
La méthode A, appelle la méthode A appelle la méthode A. Finalement, l'une de ces méthodes A n'appellera pas et ne sortira pas, mais c'est une récursion, car quelque chose s'appelle elle-même.
Exemple de récursion où je veux imprimer tous les noms de dossiers du disque dur: (en 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...
}
}
Je vais essayer de l'expliquer avec un exemple.
Tu sais quoi n! veux dire? Si non: http://en.wikipedia.org/wiki/Factorial
3! = 1 * 2 * 3 = 6
voici un pseudocode
function factorial(n) {
if (n==0) return 1
else return (n * factorial(n-1))
}
Alors essayons:
factorial(3)
est n 0?
non!
donc nous creusons plus profondément avec notre récursion:
3 * factorial(3-1)
3-1 = 2
est 2 == 0?
non!
alors on va plus loin! 3 * 2 * factorielle (2-1) 2-1 = 1
est 1 == 0?
non!
alors on va plus loin! 3 * 2 * 1 * factorielle (1-1) 1-1 = 0
est 0 == 0?
oui!
nous avons un cas trivial
nous avons donc 3 * 2 * 1 * 1 = 6
j'espère que cela vous a aidé
Quel livre utilisez-vous?
Le manuel standard sur les algorithmes qui est réellement bon est Cormen & Rivest. Mon expérience est qu’il enseigne très bien la récursivité.
La récursivité est l’un des aspects les plus difficiles à comprendre de la programmation et, même si elle nécessite un instinct, elle peut être apprise. Mais il faut une bonne description, de bons exemples et de bonnes illustrations.
En outre, 30 pages en général, c'est beaucoup, 30 pages dans un seul langage de programmation sont source de confusion. N'essayez pas d'apprendre la récursion en C ou en Java avant de comprendre la récursion en général à partir d'un livre général.
Une fonction récursive est simplement une fonction qui s’appelle autant de fois que nécessaire. C'est utile si vous devez traiter quelque chose plusieurs fois, mais vous ne savez pas combien de fois il le faudra. D'une certaine manière, vous pourriez considérer une fonction récursive comme un type de boucle. Comme une boucle, cependant, vous devrez spécifier les conditions pour que le processus soit interrompu, sinon il deviendra infini.
http://javabat.com est un endroit amusant et excitant pour pratiquer la récursion. Leurs exemples commencent assez légers et s’étendent sur une base étendue (si vous voulez aller aussi loin). Note: Leur approche est d'apprendre en pratiquant. Voici une fonction récursive que j'ai écrite pour remplacer simplement une boucle for.
La boucle for:
public printBar(length)
{
String holder = "";
for (int index = 0; i < length; i++)
{
holder += "*"
}
return holder;
}
Voici la récursion pour faire la même chose. (notez que nous surchargeons la première méthode pour nous assurer qu'elle est utilisée exactement comme ci-dessus). Nous avons également une autre méthode pour maintenir notre index (similaire à la façon dont l'instruction for le fait pour vous ci-dessus). La fonction récursive doit conserver son propre index.
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
}
Pour résumer, la récursivité est un bon moyen d'écrire moins de code. Dans ce dernier, notez que nous avons une instruction if. Si notre condition est atteinte, nous allons sortir de la récursivité et revenir à la méthode précédente, qui retourne à la méthode précédente, etc. Si j'ai envoyé un printBar (8), j'obtiens ********. J'espère qu'avec un exemple de fonction simple faisant la même chose qu'une boucle for, cela aidera peut-être. Vous pouvez pratiquer cela plus à Java Bat cependant.
Exemple récursif simple dans Common LISP:
MYMAP applique une fonction à chaque élément d'une liste.
1) une liste vide n'a pas d'élément, nous retournons donc la liste vide ) et NIL sont tous deux la liste vide.
--- (2) appliquer la fonction à la première liste, appeler MYMAP pour le reste de la liste (appel récursif) et combiner les deux résultats dans une nouvelle liste.
(DEFUN MYMAP (FUNCTION LIST)
(IF (NULL LIST)
()
(CONS (FUNCALL FUNCTION (FIRST LIST))
(MYMAP FUNCTION (REST LIST)))))
Regardons l'exécution tracée. En entrant une fonction, les arguments sont imprimés. Lors de la sortie d'une fonction, le résultat est imprimé. Pour chaque appel récursif, la sortie sera mise en retrait au niveau.
Cet exemple appelle la fonction NAS sur chaque numéro d'une liste (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)
Ceci est notre résultat:
(0.841471 0.9092975 0.14112002 -0.75680256)
Les enfants utilisent implicitement la récursivité, par exemple:
Sommes-nous là? (Non)
Sommes-nous là? (Bientôt)
Sommes-nous là? (Presque ...)
Sommes-nous là? (SHHHH)
Sommes-nous déjà là?(!!!!!)
A quel point l'enfant s'endort ...
Cette fonction de compte à rebours est un exemple simple:
function countdown()
{
return (arguments[0] > 0 ?
(
console.log(arguments[0]),countdown(arguments[0] - 1)) :
"done"
);
}
countdown(10);
loi de Hofstadter appliquée aux projets logiciels est également pertinente.
Selon Chomsky, l’essence du langage humain est la capacité des cerveaux finis à produire ce qu’il considère être des grammaires infinies. Cela signifie non seulement qu'il n'y a pas de limite supérieure à ce que nous pouvons dire, mais qu'il n'y a pas de limite supérieure pour le nombre de phrases de notre langue, il n'y a pas de limite supérieure pour la taille d'une phrase en particulier. Chomsky a affirmé que l'outil fondamental qui sous-tend toute cette créativité du langage humain est la récursivité: la possibilité pour une phrase de se reproduire dans une autre phrase du même type. Si je dis "la maison du frère de John", j'ai un nom, "maison", qui apparaît dans une expression nominale, "la maison de son frère", et cette expression nominale se trouve dans une autre expression nomique, "la maison du frère de Jean". Cela a beaucoup de sens et c'est une propriété intéressante du langage humain.
Références
Pour expliquer la récurrence à un enfant de six ans, commencez par l'expliquer à un enfant de cinq ans, puis attendez un an.
En fait, c'est un contre-exemple utile, car votre appel récursif devrait être plus simple, pas plus difficile. Il serait encore plus difficile d’expliquer la récursivité à un enfant de cinq ans, et bien que vous puissiez arrêter la récursion à 0, vous n’avez pas de solution simple pour expliquer la récursion à un enfant de zéro ans.
Pour résoudre un problème en utilisant la récursivité, commencez par le diviser en un ou plusieurs problèmes plus simples que vous pouvez résoudre de la même manière, puis lorsque le problème est suffisamment simple pour être résolu sans autre récursivité, vous pouvez: remonter à des niveaux plus élevés.
En fait, c'était une définition récursive de la façon de résoudre un problème de récursion.
La manière réellement mathématique de construire une fonction récursive serait la suivante:
1: Imaginez que vous avez une fonction correcte pour f (n-1), compilez f tel que f(n) est correct. 2: Compilez f, tel que f(1) est correct.
Voici comment vous pouvez prouver que la fonction est mathématiquement correcte et s'appelle Induction . Cela revient à avoir différents cas de base, ou des fonctions plus compliquées sur plusieurs variables). Il est également équivalent d’imaginer que f(x) est correct pour tout x
Maintenant, pour un exemple "simple". Construisez une fonction qui puisse déterminer s’il est possible d’avoir une combinaison de pièces de 5 cents et 7 cents pour gagner x cents. Par exemple, il est possible d’avoir 17 cents par 2x5 + 1x7, mais impossible d’avoir 16 cents.
Maintenant, imaginez que vous ayez une fonction qui vous indique s'il est possible de créer x cents, à condition que x <n. Appelez cette fonction can_create_coins_small. Il devrait être assez simple d’imaginer comment utiliser la fonction n. Maintenant, construisez votre fonction:
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;
}
Le truc ici est de réaliser que le fait que can_create_coins fonctionne pour n signifie que vous pouvez remplacer can_create_coins par can_create_coins_small, en donnant:
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;
}
Une dernière chose à faire est d’avoir un scénario de base pour arrêter la récursion infinie. Notez que si vous essayez de créer 0 centime, cela est possible sans pièces. L'ajout de cette condition donne:
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;
}
Il peut être prouvé que cette fonction reviendra toujours, en utilisant une méthode appelée descente infinie , mais ce n'est pas nécessaire ici. Vous pouvez imaginer que f(n) n'appelle que des valeurs inférieures de n et atteindra toujours 0.
Pour utiliser cette information afin de résoudre votre problème avec Tower of Hanoi, je pense que l’astuce consiste à supposer que vous avez la fonction de déplacer n-1 tablettes de a à b (pour tout a/b), en essayant de déplacer n tables de a à b. .
Aie. J'ai essayé de comprendre les tours de Hanoi l'année dernière. Ce qui est délicat à propos de TOH, ce n’est pas un simple exemple de récursivité: vous avez des récursions imbriquées qui changent également le rôle des tours à chaque appel. La seule façon pour que cela ait un sens était de visualiser littéralement le mouvement des cernes dans mon esprit et de verbaliser ce que serait l'appel récursif. Je commencerais par une seule sonnerie, puis deux, puis trois. J'ai effectivement commandé le jeu sur Internet. Il m'a fallu peut-être deux ou trois jours pour me fendre la cervelle.
Lorsque je travaille avec des solutions récursives, j'essaie toujours de:
En outre, il existe différents types de solutions récursives, il y a l'approche diviser pour mieux régner qui est utile pour les fractales et bien d'autres.
Il serait également utile de pouvoir travailler sur des problèmes plus simples en commençant simplement par comprendre. Quelques exemples résolvent la factorielle et génèrent le nième nombre de fibonacci.
Pour les références, je recommande fortement Algorithms de Robert Sedgewick.
J'espère que ça t'as aidé. Bonne chance.
Pensez à une abeille ouvrière. Il essaie de faire du miel. Il fait son travail et s'attend à ce que les autres abeilles ouvrières fassent le reste du miel. Et quand le nid d'abeilles est plein, ça s'arrête.
Pensez-y comme par magie. Vous avez une fonction qui porte le même nom que celle que vous essayez d'implémenter et lorsque vous lui donnez le sous-problème, elle le résout pour vous et la seule chose que vous devez faire est d'intégrer la solution de votre pièce à celle-ci. vous a donné.
Par exemple, nous voulons calculer la longueur d'une liste. Appelons notre fonction magical_length et notre assistant magique avec magical_length. Nous savons que si nous donnons la sous-liste qui n’a pas le premier élément, elle nous donnera la longueur de la sous-liste par magie. La seule chose à laquelle nous devons réfléchir est donc de savoir comment intégrer ces informations à notre travail. La longueur du premier élément est 1 et magic_counter nous donne la longueur de la sous-liste n-1, donc la longueur totale est (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
Cependant, cette réponse est incomplète car nous n'avons pas examiné ce qui se passe si nous donnons une liste vide. Nous pensions que la liste que nous avons a toujours au moins un élément. Par conséquent, nous devons réfléchir à ce qui devrait être la réponse si une liste vide nous est donnée et que la réponse est évidemment 0. Ajoutez donc cette information à notre fonction et cela s'appelle une condition de base/Edge.
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
Une fonction récursive est comme un ressort que vous compressez un peu à chaque appel. A chaque étape, vous mettez une information (contexte actuel) sur une pile. Lorsque l'étape finale est atteinte, le ressort est libéré, en collectant toutes les valeurs (contextes) à la fois!
Pas sûr que cette métaphore soit efficace ... :-)
Quoi qu’il en soit, au-delà des exemples classiques (factoriel qui est le pire exemple car il est inefficace et facilement aplati, Fibonacci, Hanoi ...) qui sont un peu artificiels (j’utilise rarement, voire jamais, dans de vrais cas de programmation), c’est intéressant de voir où il est vraiment utilisé.
Un cas très courant consiste à marcher dans un arbre (ou un graphique, mais les arbres sont plus communs, en général).
Par exemple, une hiérarchie de dossiers: pour répertorier les fichiers, vous les parcourez. Si vous trouvez un sous-répertoire, la fonction répertoriant les fichiers s’appelle avec le nouveau dossier en argument. En revenant de la liste de ce nouveau dossier (et de ses sous-dossiers!), Il reprend son contexte, jusqu'au fichier (ou dossier) suivant.
Un autre cas concret concerne l’établissement d’une hiérarchie des composants de l’interface graphique: il est courant d’avoir des conteneurs, comme des panneaux, pour contenir des composants qui peuvent également être des panneaux, ou des composants composés, etc. Le programme de dessin appelle de manière récursive la fonction Paint. de chaque composant, qui appelle la fonction Paint de tous les composants qu'il contient, etc.
Je ne suis pas sûr d'être très clair, mais j'aime montrer que le matériel didactique est utilisé dans le monde réel, car c'était quelque chose que j'avais découvert par le passé.