Comprendre le fonctionnement des fonctions récursives

115

Comme le titre l'explique, j'ai une question de programmation très fondamentale que je n'ai tout simplement pas encore pu résoudre. Filtrer tous les (extrêmement intelligents) "Pour comprendre la récursivité, vous devez d'abord comprendre la récursivité." réponses de divers fils en ligne Je ne comprends toujours pas tout à fait.

Comprenant que lorsque nous ne savons pas ce que nous ne savons pas, nous pouvons avoir tendance à poser les mauvaises questions ou à poser les bonnes questions de manière incorrecte, je vais partager ce que je "pense" ma question est dans l'espoir que quelqu'un avec des perspectives similaires puisse en partager peu de connaissances qui aideront à allumer l'ampoule récursive pour moi!

Voici la fonction (la syntaxe est écrite en Swift):

func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a: a + 1, b: b)
    }
}

Nous utiliserons 2 et 5 comme arguments:

println(sumInts(a: 2, b: 5))

De toute évidence, la réponse est 14. Mais je ne sais pas exactement comment cette valeur est atteinte.

Ce sont mes 2 raccords:

  1. La fonction est appelée récursivement jusqu'à ce qu'une condition soit remplie. Cette condition est a> b. Lorsque cette condition est remplie, retournez 0. À première vue, je m'attendrais à ce que la valeur de retour soit 0, ce qui est évidemment incorrect.

  2. Imprimer la valeur de 'a' à chaque itération donne une valeur à laquelle je m'attendrais: 2, 3, 4, 5 (à quel point 5 + 1> b qui remplit la première condition: a> b) mais je ne le fais toujours pas ' t voir comment la valeur de 14 est atteinte.

Ma première pensée est que quelque chose de similaire à ce qui suit se passe comme par magie:

var answer = a;
answer += a+1 until a > b;
return answer;   

Alors excluant la magie, je n'obtiens simplement rien. J'adorerais comprendre ce qui se passe plus qu'implicitement.

Si quelqu'un pouvait gentiment expliquer ce qui se passe techniquement pendant ce type de fonction et pourquoi le résultat n'est pas 0 et comment, finalement a + sumInts(a: a + 1, b: b) = 14, je serais à jamais redevable.

Jason Elwood
la source
7
La récursivité est l'un de ces concepts de programmation qui sont beaucoup plus faciles à comprendre en termes mathématiques qu'en code; il y a une bonne définition ici
blgt
5
LearnYouARecursion, des ensembles de problèmes complets de professeur de classe mondiale!
recursion.ninja
15
Je dois juste vous inciter à taper "Récursivité" dans le champ de recherche Google. Un de ces œufs de Pâques. Je ne vais pas gâcher la surprise pour vous.
Floris
7
Copie possible de stackoverflow.com/questions/25676961/…
Neil McGuigan

Réponses:

107

Je pense que la confusion vient du fait que l'on pense que «la même fonction» est appelée plusieurs fois. Si vous pensez qu'il s'agit de "plusieurs copies de la même fonction appelée", cela peut être plus clair:

Une seule copie de la fonction retourne jamais 0, et ce n'est pas la première (c'est la dernière). Ainsi, le résultat de l'appel du premier n'est pas 0.

Pour la deuxième confusion, je pense qu'il sera plus facile de préciser la récursivité en anglais. Lisez cette ligne:

return a + sumInts(a + 1, b: b)

comme "renvoie la valeur de 'a' plus (la valeur de retour d'une autre copie de la fonction, qui est la valeur de la copie de 'a' plus (la valeur de retour d'une autre copie de la fonction, qui est la valeur de la deuxième copie de ' a 'plus (... ", chaque copie de la fonction engendrant une nouvelle copie d'elle-même avec a augmenté de 1, jusqu'à ce que la condition a> b soit remplie.

Au moment où vous atteignez la condition a> b étant vraie, vous avez une longue pile (potentiellement arbitraire) de copies de la fonction en cours d'exécution, toutes en attente du résultat de la copie suivante pour savoir ce qu'elles devrait ajouter à «a».

(edit: aussi, quelque chose à savoir, c'est que la pile de copies de la fonction que je mentionne est une chose réelle qui prend de la vraie mémoire, et plantera votre programme s'il devient trop volumineux. Le compilateur peut l'optimiser dans certains cas, mais l'épuisement de l'espace de pile est une limitation significative et malheureuse des fonctions récursives dans de nombreux langages)

Catfish_Man
la source
7
Catfish_Man: Je pense que vous avez réussi! Penser à cela comme plusieurs «copies» de la même fonction est tout à fait logique. J'en ai encore la tête mais je pense que tu m'as envoyé sur le bon chemin! Merci de prendre le temps de votre journée bien remplie pour aider un collègue programmeur! Je marquerai votre réponse comme la bonne réponse. Passez une bonne journée!
Jason Elwood
13
C'est une bonne analogie - mais attention à ne pas le prendre trop littéralement car chaque "copie" est en fait exactement le même code. Ce qui est différent pour chaque copie, ce sont toutes les données sur lesquelles il travaille.
Tim B
2
Je ne suis pas trop content d'y penser comme une copie. Je trouve qu'une explication plus intuitive consiste à différencier la fonction elle-même (le code, ce qu'elle fait) et une invocation de fonction (instanciation de cette fonction) à laquelle un cadre de pile / contexte d'exécution est associé. La fonction ne possède pas ses variables locales, elles sont instanciées lorsque la fonction est appelée (invoquée). Mais je suppose que cela fera office d'introduction à la récursivité
Thomas
5
La terminologie correcte est qu'il y a plusieurs invocations de la fonction. Chaque appel a ses propres instances de variables aet b.
Theodore Norvell
6
Oui, il y a une quantité importante de précision qui pourrait être ajoutée à cette réponse. J'ai volontairement laissé de côté la distinction entre «instances d'une fonction» et «enregistrements d'activation des invocations d'une seule fonction», car c'était une charge conceptuelle supplémentaire qui n'aide pas vraiment à comprendre le problème. Cela aide à comprendre d' autres problèmes, donc c'est toujours des informations utiles, juste ailleurs. Ces commentaires semblent être un bon endroit pour cela :)
Catfish_Man
130

1.La fonction est appelée récursivement jusqu'à ce qu'une condition soit remplie. Cette condition est a > b. Lorsque cette condition est remplie, retournez 0. À première vue, je m'attendrais à ce que la valeur de retour soit 0, ce qui est évidemment incorrect.

Voici ce sumInts(2,5)que penserait l' informatique s'il était capable de:

I want to compute sumInts(2, 5)
for this, I need to compute sumInts(3, 5)
and add 2 to the result.
  I want to compute sumInts(3, 5)
  for this, I need to compute sumInts(4, 5)
  and add 3 to the result.
    I want to compute sumInts(4, 5)
    for this, I need to compute sumInts(5, 5)
    and add 4 to the result.
      I want to compute sumInts(5, 5)
      for this, I need to compute sumInts(6, 5)
      and add 5 to the result.
        I want to compute sumInts(6, 5)
        since 6 > 5, this is zero.
      The computation yielded 0, therefore I shall return 5 = 5 + 0.
    The computation yielded 5, therefore I shall return 9 = 4 + 5.
  The computation yielded 9, therefore I shall return 12 = 3 + 9.
The computation yielded 12, therefore I shall return 14 = 2 + 12.

Comme vous le voyez, certains appels à la fonction sumIntsretournent en fait 0 mais ce n'est pas la valeur finale car l'ordinateur doit encore ajouter 5 à ce 0, puis 4 au résultat, puis 3, puis 2, comme décrit par les quatre dernières phrases de les pensées de notre ordinateur. Notez que dans la récursivité, l'ordinateur n'a pas seulement à calculer l'appel récursif, il doit également se rappeler quoi faire avec la valeur retournée par l'appel récursif. Il y a une zone spéciale de la mémoire de l'ordinateur appelée la pile où ce type d'informations est sauvegardé, cet espace est limité et des fonctions trop récursives peuvent épuiser la pile: c'est le débordement de pile qui donne son nom à notre site Web le plus aimé.

Votre déclaration semble faire l'hypothèse implicite que l'ordinateur oublie ce à quoi il était lors d'un appel récursif, mais ce n'est pas le cas, c'est pourquoi votre conclusion ne correspond pas à votre observation.

L'impression de la valeur de `` a '' à chaque itération donne une valeur à laquelle je m'attendrais: 2, 3, 4, 5 (à quel point 5 + 1> b qui remplit la première condition: a> b) mais je reste ne vois pas comment la valeur de 14 est atteinte.

Cela est dû au fait que la valeur de retour n'est pas aelle-même mais la somme de la valeur de aet de la valeur renvoyée par l'appel récursif.

Michael Le Barbier Grünewald
la source
3
Merci d'avoir pris le temps de rédiger cette excellente réponse Michael! +1!
Jason Elwood
9
@JasonElwood Il est peut-être utile de modifier sumIntspour qu'il écrive réellement les «pensées de l'ordinateur». Une fois que vous avez écrit une main de ces fonctions, vous l'aurez probablement «compris»!
Michael Le Barbier Grünewald
4
C'est une bonne réponse, même si je note qu'il n'est pas nécessaire que l'activation de la fonction ait lieu sur une structure de données appelée «la pile». La récursivité pourrait être implémentée par le style de passage de continuation, auquel cas il n'y a pas du tout de pile. La pile n'est qu'une - particulièrement efficace, et donc d'usage courant - réification de la notion de continuation.
Eric Lippert
1
@EricLippert Bien que les techniques utilisées pour implémenter la récursivité soient un sujet intéressant en soi , je ne sais pas s'il serait utile pour l'OP - qui veut comprendre «comment ça marche» - d'être exposé à la variété des mécanismes utilisés. Bien que le style de passage de continuation ou les langages basés sur l'expansion (par exemple TeX et m4) ne soient pas intrinsèquement plus difficiles que les paradigmes de programmation plus courants, je n'offenserai personne en étiquetant ces «exotiques» et un petit mensonge blanc comme «cela arrive toujours sur la pile» devrait aider le PO à comprendre le concept. (Et une sorte de pile est toujours impliquée.)
Michael Le Barbier Grünewald
1
Il doit y avoir un moyen pour le logiciel de se souvenir de ce qu'il faisait, d'appeler la fonction de manière récursive, puis de revenir à cet état d'origine lorsqu'il revient. Ce mécanisme agit comme une pile, il est donc pratique de l'appeler une pile, même si une autre structure de données est utilisée.
Barmar le
48

Pour comprendre la récursivité, vous devez penser le problème d'une manière différente. Au lieu d'une grande séquence logique d'étapes qui a du sens dans son ensemble, vous prenez plutôt un gros problème et le divisez en problèmes plus petits et les résolvez, une fois que vous avez une réponse pour les sous-problèmes, vous combinez les résultats des sous-problèmes pour faire le solution au plus gros problème. Pensez à vous et à vos amis qui devez compter le nombre de billes dans un énorme seau. Vous prenez chacun un seau plus petit et allez les compter individuellement et lorsque vous avez terminé, vous additionnez les totaux ensemble. Eh bien maintenant, si chacun de vous trouve un ami et divise les seaux davantage, il vous suffit d'attendre que ces autres amis calculez leurs totaux, rapportez-les à chacun de vous, additionnez-les. Etc.

Vous devez vous rappeler que chaque fois que la fonction s'appelle elle-même de manière récursive, elle crée un nouveau contexte avec un sous-ensemble du problème, une fois que cette partie est résolue, elle est renvoyée afin que l'itération précédente puisse se terminer.

Laissez-moi vous montrer les étapes:

sumInts(a: 2, b: 5) will return: 2 + sumInts(a: 3, b: 5)
sumInts(a: 3, b: 5) will return: 3 + sumInts(a: 4, b: 5)
sumInts(a: 4, b: 5) will return: 4 + sumInts(a: 5, b: 5)
sumInts(a: 5, b: 5) will return: 5 + sumInts(a: 6, b: 5)
sumInts(a: 6, b: 5) will return: 0

une fois que sumInts (a: 6, b: 5) a été exécuté, les résultats peuvent être calculés afin de remonter la chaîne avec les résultats que vous obtenez:

 sumInts(a: 6, b: 5) = 0
 sumInts(a: 5, b: 5) = 5 + 0 = 5
 sumInts(a: 4, b: 5) = 4 + 5 = 9
 sumInts(a: 3, b: 5) = 3 + 9 = 12
 sumInts(a: 2, b: 5) = 2 + 12 = 14.

Une autre façon de représenter la structure de la récursivité:

 sumInts(a: 2, b: 5) = 2 + sumInts(a: 3, b: 5)
 sumInts(a: 2, b: 5) = 2 + 3 + sumInts(a: 4, b: 5)  
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + sumInts(a: 5, b: 5)  
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + sumInts(a: 6, b: 5)
 sumInts(a: 2, b: 5) = 2 + 3 + 4 + 5 + 0
 sumInts(a: 2, b: 5) = 14 
Rob
la source
2
Très bien dit, Rob. Vous l'avez présenté d'une manière très claire et facile à comprendre. Merci de prendre le temps!
Jason Elwood
3
C'est la représentation la plus claire de ce qui se passe, sans entrer dans la théorie et les détails techniques de celui-ci, montre clairement chaque étape de l'exécution.
Bryan
2
Je suis content. :) il n'est pas toujours facile d'expliquer ces choses. Merci pour le compliment.
Rob du
1
+1. C'est ainsi que je le décrirais, en particulier avec votre dernier exemple de structure. Il est utile de dérouler visuellement ce qui se passe.
KChaloux
40

La récursivité est un sujet délicat à comprendre et je ne pense pas pouvoir lui rendre pleinement justice ici. Au lieu de cela, je vais essayer de me concentrer sur le morceau de code particulier que vous avez ici et d'essayer de décrire à la fois l'intuition de savoir pourquoi la solution fonctionne et les mécanismes de la façon dont le code calcule son résultat.

Le code que vous avez donné ici résout le problème suivant: vous voulez connaître la somme de tous les entiers de a à b, inclus. Pour votre exemple, vous voulez la somme des nombres de 2 à 5, inclus, qui est

2 + 3 + 4 + 5

Lorsque vous essayez de résoudre un problème de manière récursive, l'une des premières étapes devrait être de déterminer comment décomposer le problème en un problème plus petit avec la même structure. Supposons donc que vous vouliez additionner les nombres de 2 à 5 inclus. Une façon de simplifier cela est de noter que la somme ci-dessus peut être réécrite comme

2 + (3 + 4 + 5)

Ici, (3 + 4 + 5) se trouve être la somme de tous les nombres entiers entre 3 et 5 inclus. En d'autres termes, si vous voulez connaître la somme de tous les entiers entre 2 et 5, commencez par calculer la somme de tous les entiers entre 3 et 5, puis ajoutez 2.

Alors, comment calculez-vous la somme de tous les nombres entiers entre 3 et 5 inclus? Eh bien, cette somme est

3 + 4 + 5

qui peut être considéré à la place comme

3 + (4 + 5)

Ici, (4 + 5) est la somme de tous les entiers compris entre 4 et 5 inclus. Donc, si vous vouliez calculer la somme de tous les nombres entre 3 et 5 inclus, vous calculeriez la somme de tous les nombres entiers entre 4 et 5, puis ajoutez 3.

Il y a un modèle ici! Si vous souhaitez calculer la somme des entiers entre a et b, inclus, vous pouvez effectuer les opérations suivantes. Tout d'abord, calculez la somme des entiers entre a + 1 et b, inclus. Ensuite, ajoutez un à ce total. Vous remarquerez que «calculer la somme des entiers entre a + 1 et b, inclus» se trouve être à peu près le même genre de problème que nous essayons déjà de résoudre, mais avec des paramètres légèrement différents. Plutôt que de calculer de a à b inclusivement, nous calculons de a + 1 à b inclus. C'est l'étape récursive - pour résoudre le plus gros problème ("somme de a à b, inclus"), nous réduisons le problème à une version plus petite de lui-même ("somme de a + 1 à b, inclus.").

Si vous regardez le code que vous avez ci-dessus, vous remarquerez qu'il y a cette étape dedans:

return a + sumInts(a + 1, b: b)

Ce code est simplement une traduction de la logique ci-dessus - si vous voulez faire la somme de a à b, inclus, commencez par additionner a + 1 à b, inclus (c'est l'appel récursif à sumInts), puis ajoutez a.

Bien sûr, cette approche ne fonctionnera pas en soi. Par exemple, comment calculeriez-vous la somme de tous les nombres entiers entre 5 et 5 inclus? Eh bien, en utilisant notre logique actuelle, vous calculeriez la somme de tous les nombres entiers entre 6 et 5 inclusivement, puis additionneriez 5. Alors, comment calculez-vous la somme de tous les nombres entiers entre 6 et 5 inclus? Eh bien, en utilisant notre logique actuelle, vous calculeriez la somme de tous les nombres entiers entre 7 et 5 inclusivement, puis ajoutez 6. Vous remarquerez un problème ici - cela continue et continue!

Dans la résolution de problèmes récursive, il doit y avoir un moyen d'arrêter de simplifier le problème et d'aller le résoudre directement. En règle générale, vous trouverez un cas simple où la réponse peut être déterminée immédiatement, puis structurez votre solution pour résoudre des cas simples directement lorsqu'ils surviennent. Ceci est généralement appelé un cas de base ou une base récursive .

Alors, quel est le cas de base dans ce problème particulier? Lorsque vous additionnez des entiers de a à b, inclus, si a est plus grand que b, alors la réponse est 0 - il n'y a pas de nombres dans la plage! Par conséquent, nous allons structurer notre solution comme suit:

  1. Si a> b, alors la réponse est 0.
  2. Sinon (a ≤ b), obtenez la réponse comme suit:
    1. Calculez la somme des entiers entre a + 1 et b.
    2. Ajoutez un pour obtenir la réponse.

Maintenant, comparez ce pseudocode à votre code réel:

func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a + 1, b: b)
    }
}

Notez qu'il y a presque exactement une carte un-à-un entre la solution décrite dans le pseudocode et ce code réel. La première étape est le cas de base - dans le cas où vous demandez la somme d'une plage vide de nombres, vous obtenez 0. Sinon, calculez la somme entre a + 1 et b, puis ajoutez a.

Jusqu'à présent, je n'ai donné qu'une idée de haut niveau derrière le code. Mais vous aviez deux autres très bonnes questions. Premièrement, pourquoi cela ne renvoie-t-il pas toujours 0, étant donné que la fonction dit de retourner 0 si a> b? Deuxièmement, d'où viennent les 14? Regardons-les à tour de rôle.

Essayons un cas très, très simple. Que se passe-t-il si vous appelez sumInts(6, 5)? Dans ce cas, en suivant le code, vous voyez que la fonction renvoie simplement 0. C'est la bonne chose à faire, pour - il n'y a pas de nombres dans la plage. Maintenant, essayez quelque chose de plus difficile. Que se passe-t-il lorsque vous appelez sumInts(5, 5)? Eh bien, voici ce qui se passe:

  1. Vous appelez sumInts(5, 5). Nous tombons dans la elsebranche, qui renvoie la valeur de `a + sumInts (6, 5).
  2. Afin sumInts(5, 5)de déterminer ce que sumInts(6, 5)c'est, nous devons mettre en pause ce que nous faisons et passer un appel sumInts(6, 5).
  3. sumInts(6, 5)est appelé. Il entre dans la ifsuccursale et revient 0. Cependant, cette instance de a sumIntsété appelée par sumInts(5, 5), donc la valeur de retour est communiquée à sumInts(5, 5), et non à l'appelant de niveau supérieur.
  4. sumInts(5, 5)maintenant peut calculer 5 + sumInts(6, 5)pour revenir 5. Il le renvoie ensuite à l'appelant de niveau supérieur.

Remarquez comment la valeur 5 a été formée ici. Nous avons commencé avec un appel actif à sumInts. Cela a déclenché un autre appel récursif et la valeur retournée par cet appel a communiqué les informations à sumInts(5, 5). L'appel à sumInts(5, 5)alors à son tour a effectué un certain calcul et renvoyé une valeur à l'appelant.

Si vous essayez ceci avec sumInts(4, 5), voici ce qui va se passer:

  • sumInts(4, 5)essaie de revenir 4 + sumInts(5, 5). Pour ce faire, il appelle sumInts(5, 5).
    • sumInts(5, 5)essaie de revenir 5 + sumInts(6, 5). Pour ce faire, il appelle sumInts(6, 5).
    • sumInts(6, 5)renvoie 0 à sumInts(5, 5).</li> <li>sumInts (5, 5) now has a value forsumInts (6, 5) , namely 0. It then returns5 + 0 = 5`.
  • sumInts(4, 5)a maintenant une valeur pour sumInts(5, 5), à savoir 5. Il retourne ensuite 4 + 5 = 9.

En d'autres termes, la valeur renvoyée est formée en additionnant les valeurs une par une, en prenant à chaque fois une valeur renvoyée par un appel récursif particulier à sumIntset en ajoutant la valeur actuelle de a. Lorsque la récursion atteint son maximum, l'appel le plus profond renvoie 0. Cependant, cette valeur ne quitte pas immédiatement la chaîne d'appels récursifs; au lieu de cela, il remet simplement la valeur à l'appel récursif un calque au-dessus. De cette façon, chaque appel récursif ajoute simplement un nombre supplémentaire et le renvoie plus haut dans la chaîne, aboutissant à la sommation globale. En tant qu'exercice, essayez de tracer cela sumInts(2, 5), c'est ce que vous vouliez commencer.

J'espère que cela t'aides!

templatetypedef
la source
3
Merci d'avoir pris le temps de votre journée bien remplie pour partager une réponse aussi complète! Il y a une tonne d'informations intéressantes ici qui m'aident à comprendre les fonctions récursives et aideront sûrement ceux qui tomberont sur ce poste à l'avenir. Merci encore et bonne journée!
Jason Elwood
22

Vous avez quelques bonnes réponses ici jusqu'à présent, mais j'en ajouterai une de plus qui adopte une approche différente.

Tout d'abord, j'ai écrit de nombreux articles sur des algorithmes récursifs simples que vous pourriez trouver intéressants; voir

http://ericlippert.com/tag/recursion/

http://blogs.msdn.com/b/ericlippert/archive/tags/recursion/

Ceux-ci sont dans l'ordre le plus récent, alors commencez par le bas.

Deuxièmement, jusqu'à présent, toutes les réponses ont décrit la sémantique récursive en considérant l'activation de fonction . Que chacun, chaque appel effectue une nouvelle activation , et que l'appel récursif s'exécute dans le cadre de cette activation. C'est une bonne façon d'y penser, mais il existe une autre façon équivalente: rechercher et remplacer du texte intelligent .

Permettez-moi de réécrire votre fonction sous une forme légèrement plus compacte; ne pensez pas à cela comme étant dans une langue particulière.

s = (a, b) => a > b ? 0 : a + s(a + 1, b)

J'espère que cela à du sens. Si vous n'êtes pas familier avec l'opérateur conditionnel, il est de la forme condition ? consequence : alternativeet sa signification deviendra claire.

Nous souhaitons maintenant évaluer s(2,5) Nous le faisons en remplaçant textuellement l'appel par le corps de la fonction, puis en remplaçant apar 2et bpar 5:

s(2, 5) 
---> 2 > 5 ? 0 : 2 + s(2 + 1, 5)

Évaluez maintenant le conditionnel. Nous remplaçons textuellement 2 > 5par false.

---> false ? 0 : 2 + s(2 + 1, 5)

Maintenant, remplacez textuellement toutes les fausses conditionnelles par l'alternative et toutes les vraies conditionnelles par la conséquence. Nous n'avons que de fausses conditions, nous remplaçons donc textuellement cette expression par l'alternative:

---> 2 + s(2 + 1, 5)

Maintenant, pour m'éviter d'avoir à taper tous ces +signes, remplacez textuellement l'arithmétique constante par sa valeur. (C'est un peu une triche, mais je ne veux pas avoir à garder une trace de toutes les parenthèses!)

---> 2 + s(3, 5)

Maintenant, recherchez et remplacez, cette fois avec le corps de l'appel, 3pour aet 5pour b. Nous mettrons le remplacement de l'appel entre parenthèses:

---> 2 + (3 > 5 ? 0 : 3 + s(3 + 1, 5))

Et maintenant, nous continuons simplement à faire ces mêmes étapes de substitution textuelle:

---> 2 + (false ? 0 : 3 + s(3 + 1, 5))  
---> 2 + (3 + s(3 + 1, 5))                
---> 2 + (3 + s(4, 5))                     
---> 2 + (3 + (4 > 5 ? 0 : 4 + s(4 + 1, 5)))
---> 2 + (3 + (false ? 0 : 4 + s(4 + 1, 5)))
---> 2 + (3 + (4 + s(4 + 1, 5)))
---> 2 + (3 + (4 + s(5, 5)))
---> 2 + (3 + (4 + (5 > 5 ? 0 : 5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (false ? 0 : 5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (5 + s(5 + 1, 5))))
---> 2 + (3 + (4 + (5 + s(6, 5))))
---> 2 + (3 + (4 + (5 + (6 > 5 ? 0 : s(6 + 1, 5)))))
---> 2 + (3 + (4 + (5 + (true ? 0 : s(6 + 1, 5)))))
---> 2 + (3 + (4 + (5 + 0)))
---> 2 + (3 + (4 + 5))
---> 2 + (3 + 9)
---> 2 + 12
---> 14

Tout ce que nous avons fait ici était juste une simple substitution textuelle . Vraiment, je n'aurais pas dû remplacer «3» par «2 + 1» et ainsi de suite jusqu'à ce que j'aie dû le faire, mais pédagogiquement, cela aurait été difficile à lire.

L'activation de fonction n'est rien de plus que de remplacer l'appel de fonction par le corps de l'appel, et de remplacer les paramètres formels par leurs arguments correspondants. Vous devez faire attention à l'introduction intelligente des parenthèses, mais à part cela, il ne s'agit que d'un remplacement de texte.

Bien sûr, la plupart des langues ne sont pas réellement mettre en œuvre l' activation en remplacement de texte, mais logiquement c'est ce qu'il est.

Alors, qu'est-ce qu'une récursion illimitée? Une récursion où la substitution textuelle ne s'arrête pas! Remarquez comment nous sommes finalement arrivés à une étape où il n'y avait plus rien sà remplacer, et nous pourrions alors simplement appliquer les règles d'arithmétique.

Eric Lippert
la source
Bon exemple mais cela vous brise le cœur lorsque vous procédez à des calculs plus complexes. Par exemple. Trouver l'ancêtre commun dans un arbre binaire.
CodeYogi
11

La façon dont je comprends généralement le fonctionnement d'une fonction récursive est de regarder le cas de base et de travailler à l'envers. Voici cette technique appliquée à cette fonction.

Tout d'abord le cas de base:

sumInts(6, 5) = 0

Ensuite, l'appel juste au-dessus dans la pile d'appels :

sumInts(5, 5) == 5 + sumInts(6, 5)
sumInts(5, 5) == 5 + 0
sumInts(5, 5) == 5

Ensuite, l'appel juste au-dessus dans la pile d'appels:

sumInts(4, 5) == 4 + sumInts(5, 5)
sumInts(4, 5) == 4 + 5
sumInts(4, 5) == 9

Etc:

sumInts(3, 5) == 3 + sumInts(4, 5)
sumInts(3, 5) == 3 + 9
sumInts(3, 5) == 12

Etc:

sumInts(2, 5) == 2 + sumInts(3, 5)
sumInts(4, 5) == 2 + 12
sumInts(4, 5) == 14

Notez que nous sommes arrivés à notre appel initial à la fonction sumInts(2, 5) == 14

L'ordre dans lequel ces appels sont exécutés:

sumInts(2, 5)
sumInts(3, 5)
sumInts(4, 5)
sumInts(5, 5)
sumInts(6, 5)

L'ordre dans lequel ces appels reviennent:

sumInts(6, 5)
sumInts(5, 5)
sumInts(4, 5)
sumInts(3, 5)
sumInts(2, 5)

Notez que nous sommes arrivés à une conclusion sur le fonctionnement de la fonction en traçant les appels dans l'ordre dans lequel ils retournent .

Piste de l'Oregon
la source
5

Je vais essayer.

En exécutant l'équation a + sumInts (a + 1, b), je vais montrer comment la réponse finale est 14.

//the sumInts function definition
func sumInts(a: Int, b: Int) -> Int {
    if (a > b) {
        return 0
    } else {
        return a + sumInts(a + 1, b)
    }
}

Given: a = 2 and b = 5

1) 2 + sumInts(2+1, 5)

2) sumInts(3, 5) = 12
   i) 3 + sumInts(3+1, 5)
   ii) 4 + sumInts(4+1, 5)
   iii) 5 + sumInts(5+1, 5)
   iv) return 0
   v) return 5 + 0
   vi) return 4 + 5
   vii) return 3 + 9

3) 2 + 12 = 14.

Faites-nous savoir si vous avez d'autres questions.

Voici un autre exemple de fonctions récursives dans l'exemple suivant.

Un homme vient de terminer ses études collégiales.

t est la durée en années.

Le nombre total réel d'années travaillées avant la retraite peut être calculé comme suit:

public class DoIReallyWantToKnow 
{
    public int howLongDoIHaveToWork(int currentAge)
    {
      const int DESIRED_RETIREMENT_AGE = 65;
      double collectedMoney = 0.00; //remember, you just graduated college
      double neededMoneyToRetire = 1000000.00

      t = 0;
      return work(t+1);
    }

    public int work(int time)
    {
      collectedMoney = getCollectedMoney();

      if(currentAge >= DESIRED_RETIREMENT_AGE 
          && collectedMoney == neededMoneyToRetire
      {
        return time;
      }

      return work(time + 1);
    }
}

Et cela devrait suffire à déprimer n'importe qui, lol. ;-P

Bryan
la source
5

Récursivité. En informatique, la récursivité est traitée en profondeur sous le thème des automates finis.

Dans sa forme la plus simple, c'est une auto-référence. Par exemple, dire que "ma voiture est une voiture" est une déclaration récursive. Le problème est que l'instruction est une récursion infinie en ce qu'elle ne finira jamais. La définition dans l'énoncé d'une «voiture» est qu'il s'agit d'une «voiture» et peut donc être remplacée. Cependant, il n'y a pas de fin car en cas de substitution, cela devient quand même "ma voiture est une voiture".

Cela pourrait être différent si la déclaration était "ma voiture est une bentley. Ma voiture est bleue". Dans ce cas, la substitution dans la deuxième situation pour la voiture pourrait être "bentley" résultant en "ma bentley est bleue". Ces types de substitutions sont expliqués mathématiquement en informatique par des grammaires sans contexte .

La substitution réelle est une règle de production. Étant donné que l'énoncé est représenté par S et que la voiture est une variable qui peut être un «bentley», cet énoncé peut être reconstruit de manière récursive.

S -> "my"S | " "S | CS | "is"S | "blue"S | ε
C -> "bentley"

Cela peut être construit de plusieurs manières, car chacune |signifie qu'il y a un choix. Speut être remplacé par n'importe lequel de ces choix, et S commence toujours vide. Les εmoyens de mettre fin à la production. Tout comme Speuvent être remplacées, d'autres variables peuvent l'être également (il n'y en a qu'une etC ce qui représenterait "bentley").

Donc commencer par Sêtre vide, et le remplacer par le premier choix"my"S S devient

"my"S

Speut toujours être substitué car il représente une variable. Nous pourrions choisir à nouveau "mon", ou ε pour le terminer, mais continuons à faire notre déclaration originale. Nous choisissons l'espace qui signifieS est remplacé par" "S

"my "S

Ensuite, choisissons C

"my "CS

Et C n'a qu'un seul choix pour le remplacement

"my bentley"S

Et de nouveau l'espace pour S

"my bentley "S

Et ainsi de suite "my bentley is"S,"my bentley is "S , "my bentley is blue"S,"my bentley is blue" (remplaçant S pour ε se termine la production) et nous avons construit notre récursive déclaration « mon bentley est bleu ».

Pensez à la récursivité comme à ces productions et remplacements. Chaque étape du processus remplace son prédécesseur afin de produire le résultat final. Dans l'exemple exact de la somme récursive de 2 à 5, vous vous retrouvez avec la production

S -> 2 + A
A -> 3 + B
B -> 4 + C
C -> 5 + D
D -> 0

Cela devient

2 + A
2 + 3 + B
2 + 3 + 4 + C
2 + 3 + 4 + 5 + D
2 + 3 + 4 + 5 + 0
14
Travis J
la source
Je ne suis pas sûr que les automates à états finis ou les grammaires sans contexte soient les meilleurs exemples qui peuvent aider à construire une première intuition sur la récursivité. Ce sont de beaux exemples, mais peut-être un peu inconnus des programmeurs n'ayant aucune expérience en CS.
chi
4

Je pense que la meilleure façon de comprendre les fonctions récursives est de réaliser qu'elles sont conçues pour traiter des structures de données récursives. Mais dans votre fonction d'origine sumInts(a: Int, b: Int)qui calcule récursivement la somme des nombres de aà b, cela ne semble pas être une structure de données récursive ... Essayons une version légèrement modifiée sumInts(a: Int, n: Int)nest le nombre de nombres que vous ajouterez.

Maintenant, sumInts est récursif sur n, un nombre naturel. Toujours pas une donnée récursive, non? Eh bien, un nombre naturel pourrait être considéré comme une structure de données récursive utilisant les axiomes Peano:

enum Natural = {
    case Zero
    case Successor(Natural)
}

Donc, 0 = zéro, 1 = successeur (zéro), 2 = successeur (successeur (zéro)), et ainsi de suite.

Une fois que vous avez une structure de données récursive, vous avez le modèle pour la fonction. Pour chaque cas non récursif, vous pouvez calculer la valeur directement. Pour les cas récursifs, vous supposez que la fonction récursive fonctionne déjà et vous l'utilisez pour calculer le cas, mais en déconstruisant l'argument. Dans le cas de Natural, cela signifie qu'au lieu de Succesor(n)nous utiliserons n, ou de manière équivalente, au lieu de nnous utiliserons n - 1.

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
    if (n == 0) {
        // non recursive case
    } else {
        // recursive case. We use sumInts(..., n - 1)
    }
}

Désormais, la fonction récursive est plus simple à programmer. Tout d' abord, le cas de base, n=0. Que devons-nous retourner si nous ne voulons ajouter aucun chiffre? La réponse est bien sûr 0.

Qu'en est-il du cas récursif? Si nous voulons ajouter des nnombres commençant par aet nous avons déjà une sumIntsfonction de travail qui fonctionne pour n-1? Eh bien, nous devons ajouter apuis invoquer sumIntsavec a + 1, donc nous finissons par:

// sums n numbers beginning from a
func sumInts(a: Int, n: Int) -> Int {
    if (n == 0) {
        return 0
    } else {
        return a + sumInts(a + 1, n - 1)
    }
}

La bonne chose est que maintenant vous ne devriez pas avoir besoin de penser au faible niveau de récursivité. Il vous suffit de vérifier que:

  • Pour les cas de base des données récursives, il calcule la réponse sans utiliser la récursivité.
  • Pour les cas récursifs des données récursives, il calcule la réponse en utilisant la récursivité sur les données déstructurées.
jdinunzio
la source
4

Vous pourriez être intéressé par l' implémentation des fonctions de Nisan et Schocken . Le pdf lié fait partie d'un cours en ligne gratuit. Il décrit la deuxième partie d'une implémentation de machine virtuelle dans laquelle l'étudiant doit écrire un compilateur de langage de machine virtuelle à langage de machine. L'implémentation de fonction qu'ils proposent est capable de récursivité car elle est basée sur la pile.

Pour vous présenter l'implémentation de la fonction: Considérez le code de machine virtuelle suivant:

entrez la description de l'image ici

Si Swift compilé dans ce langage de machine virtuelle, alors le bloc suivant de code Swift:

mult(a: 2, b: 3) - 4

compilerait jusqu'à

push constant 2  // Line 1
push constant 3  // Line 2
call mult        // Line 3
push constant 4  // Line 4
sub              // Line 5

Le langage de la machine virtuelle est conçu autour d'une pile globale .push constant npousse un entier sur cette pile globale.

Après avoir exécuté les lignes 1 et 2, la pile ressemble à:

256:  2  // Argument 0
257:  3  // Argument 1

256et 257sont des adresses de mémoire.

call mult pousse le numéro de la ligne de retour (3) sur la pile et alloue de l'espace pour les variables locales de la fonction.

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  0  // local 0

... et ça va au label function mult. Le code à l'intérieur multest exécuté. À la suite de l'exécution de ce code, nous calculons le produit de 2 et 3, qui est stocké dans la 0ème variable locale de la fonction.

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0

Juste avant returnd'entrer de mult, vous remarquerez la ligne:

push local 0  // push result

Nous allons pousser le produit sur la pile.

256:  2  // argument 0
257:  3  // argument 1
258:  3  // return line number
259:  6  // local 0
260:  6  // product

À notre retour, ce qui suit se produit:

  • Pop la dernière valeur de la pile à l'adresse mémoire du 0ème argument (256 dans ce cas). C'est l'endroit le plus pratique pour le mettre.
  • Supprimez tout ce qui se trouve sur la pile jusqu'à l'adresse du 0ème argument.
  • Allez au numéro de ligne de retour (3 dans ce cas), puis avancez.

Après le retour, nous sommes prêts à exécuter la ligne 4, et notre pile ressemble à ceci:

256:  6  // product that we just returned

Maintenant, nous poussons 4 sur la pile.

256:  6
257:  4

subest une fonction primitive du langage de la machine virtuelle. Il prend deux arguments et renvoie son résultat à l'adresse habituelle: celle du 0ème argument.

Maintenant nous avons

256:  2  // 6 - 4 = 2

Maintenant que vous savez comment fonctionne un appel de fonction, il est relativement simple de comprendre le fonctionnement de la récursivité. Pas de magie , juste une pile.

J'ai implémenté votre sumIntsfonction dans ce langage de machine virtuelle:

function sumInts 0     // `0` means it has no local variables.
  label IF
    push argument 0
    push argument 1
    lte              
    if-goto ELSE_CASE
    push constant 0
    return
  label ELSE_CASE
    push constant 2
    push argument 0
    push constant 1
    add
    push argument 1
    call sumInts       // Line 15
    add                // Line 16
    return             // Line 17
// End of function

Maintenant, je vais l'appeler:

push constant 2
push constant 5
call sumInts           // Line 21

Le code s'exécute et nous arrivons jusqu'au point d'arrêt où lterevient false. Voici à quoi ressemble la pile à ce stade:

// First invocation
256:  2   // argument 0
257:  5   // argument 1
258:  21  // return line number
259:  2   // augend
// Second
260:  3   // argument 0
261:  5   // argument 1
262:  15  // return line number
263:  3   // augend
// Third
264:  4   // argument 0
265:  5   // argument 1
266:  15  // return line number
267:  4   // augend
// Fourth
268:  5   // argument 0
269:  5   // argument 1
270:  15  // return line number
271:  5   // augend
// Fifth
272:  6   // argument 0
273:  5   // argument 1
274:  15  // return line number
275:  0   // return value

Maintenant, "déroulons" notre récursivité. return0 et aller à la ligne 15 et avancer.

271:  5
272:  0

Ligne 16: add

271:  5

Ligne 17: return5 et aller à la ligne 15 et avancer.

267:  4
268:  5

Ligne 16: add

267:  9

Ligne 17: return9 et aller à la ligne 15 et avancer.

263:  3
264:  9

Ligne 16: add

263:  12

Ligne return17:12 et aller à la ligne 15 et avancer.

259:  2
260:  12

Ligne 16: add

259:  14

Ligne return17:14 et aller à la ligne 21 et avancer.

256:  14

Voilà. Récursivité: glorifiée goto.

Jackson
la source
4

Un très bon conseil que j'ai rencontré en apprenant et en comprenant vraiment la récursivité est de passer du temps à apprendre un langage qui n'a aucune forme de construction de boucle autre que la récursivité. De cette façon, vous aurez une bonne idée de la façon d'utiliser la récursivité via la pratique.

J'ai suivi http://www.htdp.org/ qui, en plus d'être un didacticiel Scheme, est également une excellente introduction sur la façon de concevoir des programmes en termes d'architecture et de design.

Mais fondamentalement, vous devez investir du temps. Sans une compréhension «ferme» de la récursivité, certains algorithmes, comme le retour en arrière, vous sembleront toujours «difficiles», voire «magiques». Alors, persévérez. :-RÉ

J'espère que ça aide et bonne chance!

Th3Minstr3l
la source
3

Il y a déjà beaucoup de bonnes réponses. Je suis encore en train d'essayer.
Lorsqu'elle est appelée, une fonction obtient un espace mémoire alloué, qui est empilé sur l' espace mémoire de la fonction appelante. Dans cet espace mémoire, la fonction conserve les paramètres qui lui sont passés, les variables et leurs valeurs. Cet espace mémoire disparaît avec l'appel de retour de fin de la fonction. Comme l'idée de pile va, l' espace mémoire de la fonction appelant devient maintenant actif.

Pour les appels récursifs, la même fonction obtient plusieurs espaces mémoire empilés les uns sur les autres. C'est tout. L'idée simple du fonctionnement de la pile dans la mémoire d'un ordinateur devrait vous permettre de comprendre comment la récursivité se produit dans l'implémentation.

Gulshan
la source
3

Un peu hors sujet, je sais, mais ... essayez de rechercher la récursivité dans Google ... Vous verrez par exemple ce que cela signifie :-)


Les versions antérieures de Google ont renvoyé le texte suivant (cité de mémoire):

Récursion

Voir la récursivité

Le 10 septembre 2014, la blague sur la récursivité a été mise à jour:

Récursion

Vouliez-vous dire: récursion


Pour une autre réponse, voir cette réponse .

Pierre Arnaud
la source
3

Pensez à la récursivité comme à plusieurs clones faisant la même chose ...

Vous demandez de cloner [1]: "somme des nombres entre 2 et 5"

+ clone[1]               knows that: result is 2 + "sum numbers between 3 and 5". so he asks to clone[2] to return: "sum numbers between 3 and 5"
|   + clone[2]           knows that: result is 3 + "sum numbers between 4 and 5". so he asks to clone[3] to return: "sum numbers between 4 and 5"
|   |   + clone[3]       knows that: result is 4 + "sum numbers between 5 and 5". so he asks to clone[4] to return: "sum numbers between 5 and 5"
|   |   |   + clone[4]   knows that: result is 5 + "sum numbers between 6 and 5". so he asks to clone[5] to return: "sum numbers between 6 and 5"
|   |   |   |   clone[5] knows that: he can't sum, because 6 is larger than 5. so he returns 0 as result.
|   |   |   + clone[4]   gets the result from clone[5] (=0)  and sums: 5 + 0,  returning 5
|   |   + clone[3]       gets the result from clone[4] (=5)  and sums: 4 + 5,  returning 9
|   + clone[2]           gets the result from clone[3] (=9)  and sums: 3 + 9,  returning 12
+ clone[1]               gets the result from clone[2] (=12) and sums: 2 + 12, returning 14

et voilá !!

Christian
la source
2

Bon nombre des réponses ci-dessus sont très bonnes. Cependant, une technique utile pour résoudre la récursivité consiste à définir d'abord ce que nous voulons faire et à coder comme un humain le résoudrait. Dans le cas ci-dessus, nous voulons résumer une séquence d'entiers consécutifs (en utilisant les nombres ci-dessus):

2, 3, 4, 5  //adding these numbers would sum to 14

Maintenant, notez que ces lignes sont déroutantes (pas fausses, mais déroutantes).

if (a > b) {
    return 0 
}

Pourquoi le test a>b ?, Et pourquoireturn 0

Changeons le code pour refléter plus étroitement ce que fait un humain

func sumInts(a: Int, b: Int) -> Int {
  if (a == b) {
    return b // When 'a equals b' I'm at the most Right integer, return it
  }
  else {
    return a + sumInts(a: a + 1, b: b)
  }
}

Pouvons-nous le faire encore plus humainement? Oui! Habituellement, nous résumons de gauche à droite (2 + 3 + ...). Mais la récursion ci-dessus est la somme de droite à gauche (... + 4 + 5). Changer le code pour le refléter (Cela -peut être un peu intimidant, mais pas beaucoup)

func sumInts(a: Int, b: Int) -> Int {
  if (a == b) {
    return b // When I'm at the most Left integer, return it
  }
  else {
    return sumInts(a: a, b: b - 1) + b
  }
}

Certains peuvent trouver cette fonction plus déroutante car nous partons de l'extrémité `` éloignée '', mais la pratique peut la rendre naturelle (et c'est une autre bonne technique de `` réflexion '': essayer les `` deux '' côtés lors de la résolution d'une récursivité). Et encore une fois, la fonction reflète ce que fait un humain (le plus?): Prend la somme de tous les entiers de gauche et ajoute le «prochain» entier à droite.

user6085241
la source
2

J'avais du mal à comprendre la récursivité alors j'ai trouvé ce blog et j'ai déjà vu cette question alors j'ai pensé que je devais partager. Vous devez lire ce blog, j'ai trouvé cela extrêmement utile, il explique avec pile et même il explique comment deux récursions fonctionnent avec la pile étape par étape. Je vous recommande d'abord de comprendre le fonctionnement de la pile, ce que cela explique très bien ici: Journey-to-the-stack

then now you will understand how recursion works now take a look of this post: Comprendre la récursivité étape par étape

entrez la description de l'image ici

C'est un programme:

def hello(x):
    if x==1:
        return "op"
    else:
        u=1
        e=12
        s=hello(x-1)
        e+=1
        print(s)
        print(x)
        u+=1
    return e

hello(3)

entrez la description de l'image ici entrez la description de l'image ici


la source
2

La récursivité a commencé à avoir un sens pour moi lorsque j'ai arrêté de lire ce que les autres en disent ou que je la vois comme quelque chose que je peux éviter et que j'écris du code. J'ai trouvé un problème avec une solution et j'ai essayé de dupliquer la solution sans chercher. Je n'ai regardé la solution que lorsque je suis resté impuissant. Puis je suis retourné à essayer de le dupliquer. Je l'ai fait à nouveau sur plusieurs problèmes jusqu'à ce que je développe ma propre compréhension et mon sens de la façon d'identifier un problème récursif et de le résoudre. Quand je suis arrivé à ce niveau, j'ai commencé à inventer des problèmes et à les résoudre. Cela m'a aidé davantage. Parfois, les choses ne peuvent être apprises qu'en essayant par vous-même et en luttant; jusqu'à ce que vous «compreniez».

Phil
la source
0

Laissez-moi vous dire avec un exemple de la série Fibonacci, Fibonacci est

t (n) = t (n - 1) + n;

si n = 0 alors 1

donc permettre de voir comment fonctionne récursives, je remplace simplement nen t(n)avec n-1et ainsi de suite. ça ressemble:

t (n-1) = t (n - 2) + n + 1;

t (n-1) = t (n - 3) + n + 1 + n;

t (n-1) = t (n - 4) + n + 1 + n + 2 + n;

.

.

.

t (n) = t (nk) + ... + (nk-3) + (nk-2) + (nk-1) + n;

nous savons si t(0)=(n-k)égaux à 1alors n-k=0si n=knous remplaçons kavec n:

t (n) = t (nn) + ... + (n-n + 3) + (n-n + 2) + (n-n + 1) + n;

si nous omettons n-nalors:

t (n) = t (0) + ... + 3 + 2 + 1 + (n-1) + n;

il en 3+2+1+(n-1)+nest de même pour le nombre naturel. il se calcule commeΣ3+2+1+(n-1)+n = n(n+1)/2 => n²+n/2

le résultat pour fib est: O(1 + n²) = O(n²)

C'est la meilleure façon de comprendre la relation récursive

Alireza Rahmani Khalili
la source