web-dev-qa-db-fra.com

Comprendre le fonctionnement des fonctions récursives

Comme le titre l'indique, j'ai une question de programmation très fondamentale à laquelle je n'ai pas encore pu répondre. Filtrer tous les (extrêmement astucieux) "Pour comprendre la récursion, vous devez d’abord comprendre la récursivité." réponses de divers fils de discussion en ligne, je ne l’ai toujours pas bien compris. 

Comprenant que, confrontés à ne pas savoir 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 un Outlook similaire puisse peu de connaissances qui m'aideront à allumer l'ampoule récursive!

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))

Évidemment, la réponse est 14. Mais je ne vois pas comment cette valeur est atteinte.

Ce sont mes 2 hangups:

  1. La fonction est appelée de manière récursive jusqu'à ce qu'une condition soit remplie. Cette condition est un> b. Lorsque cette condition est remplie, renvoyez 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 vois comment la valeur de 14 est atteinte. 

Ma première pensée est que quelque chose de semblable au suivant se passe comme par magie:

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

Donc, en excluant la magie, je ne comprends rien. J'aimerais comprendre ce qui se passe plus qu'implicitement. 

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

111
Jason Elwood

Je pense la confusion provient de penser que "la même fonction" est appelée à plusieurs reprises. Si vous le considérez comme "plusieurs copies de la même fonction étant appelée", cela peut être plus clair:

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

Pour le second brouillon, je pense qu’il sera plus facile d’écrire la récursion en anglais. Lire cette ligne:

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

comme "renvoyer la valeur de 'a' (la valeur de retour d'une autre copie de la fonction, qui est la valeur de la copie de 'a' (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 générant une nouvelle copie de lui-même augmentée de 1, jusqu'à ce que la condition a> b soit remplie.

Lorsque vous atteignez la condition a> b, vous disposez d’une pile (potentiellement arbitraire) de copies de la fonction, toutes en cours d’exécution, toutes attendant le résultat de la copie suivante pour savoir ce qu’elles devrait ajouter à 'a'.

(edit: aussi, il faut savoir que la pile de copies de la fonction que je mentionne est une chose réelle qui prend de la mémoire, et plantera votre programme si il est trop volumineux. Le compilateur peut l’optimiser dans certains cas. cas, mais épuiser l’espace de pile est une limitation importante et malheureuse des fonctions récursives dans de nombreuses langues)

104
Catfish_Man

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

Voici ce que l'ordinateur sumInts(2,5) penserait 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 sumInts renvoient en réalité 0 mais ceci n'est pas la valeur finale car l'ordinateur doit encore ajouter 5 à celui-ci, puis 4 au résultat, puis 3, puis 2, comme décrit dans les quatre dernières phrases. des pensées de notre ordinateur. Notez que dans la récursivité, l'ordinateur doit non seulement calculer l'appel récursif, il doit également se rappeler quoi faire avec la valeur renvoyée par l'appel récursif. Il existe une zone spéciale de la mémoire de l’ordinateur appelée la pile où ce type d’informations est enregistré, cet espace est limité et des fonctions trop récursives peuvent épuiser la pile: c’est le débordement de pile donnant son nom sur notre site le plus aimé.

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

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

En effet, la valeur de retour n'est pas une a elle-même, mais la somme de la valeur de a et de la valeur renvoyée par l'appel récursif.

Pour comprendre la récursivité, vous devez penser le problème différemment. 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 résolvez-les, une fois que vous avez une réponse aux sous-problèmes, vous combinez les résultats des solution au plus gros problème. Pensez à vous et à vos amis qui devez compter le nombre de billes dans un grand seau. Vous prenez chacun un seau plus petit et allez les compter individuellement et lorsque vous avez terminé, vous additionnez les totaux ensemble. Bien maintenant, si chacun de vous trouve un ami et sépare les seaux plus loin, il vous suffit d'attendre que ces autres amis calculer leurs totaux, ramenez-les à chacun de vous, additionnez-les. Etc. Le cas particulier est que lorsque vous ne comptez que 1 bille, vous la restituez et dites: 1. laissez les autres personnes au-dessus de vous faire l'ajout que vous avez fait.

Vous devez vous rappeler que chaque fois que la fonction s’appelle de manière récursive, elle crée un nouveau contexte avec un sous-ensemble du problème. Une fois cette partie résolue, elle est renvoyée de sorte 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. Vous pouvez donc remonter dans la chaîne avec les résultats obtenus:

 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écursion:

 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 
46
Rob

Vous avez obtenu de bonnes réponses jusqu'à présent, mais je vais en ajouter 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/

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

Deuxièmement, jusqu'à présent, toutes les réponses ont décrit la sémantique récursive en considérant activation de la fonction. Que chacun, chaque appel effectue une nouvelle activation, et l'appel récursif s'exécute dans le contexte de cette activation. C’est une bonne façon de penser à cela, mais il existe un autre moyen équivalent: smart text seach-and-replace.

Me laisser réécrire votre fonction dans 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 : alternative et 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 nous remplaçons a par 2 et b par 5:

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

Maintenant, évaluez le conditionnel. Nous remplaçons textuellement 2 > 5 par false.

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

Maintenant, remplacez textuellement tous les faux conditionnels par l'alternative et tous les vrais conditionnels avec la conséquence. Nous n'avons que de fausses conditions, donc textuellement nous remplaçons cette expression par l'alternative:

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

Maintenant, pour éviter de devoir 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-ci avec le corps de l'appel, 3 pour a et 5 pour 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 les 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 n’est qu’une simple substitution textuelle. En réalité, je n'aurais pas dû substituer "3" à "2 + 1" et ainsi de suite jusqu'à ce que je sois obligé, mais pédagogiquement, cela aurait été difficile à lire. 

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

Bien sûr, la plupart des langues n'implémentent pas implémentent l'activation en remplacement du texte, mais logiquement c'est ce que c'est.

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

22
Eric Lippert

Pour comprendre le fonctionnement d’une fonction récursive, j’ai l'habitude de regarder le scénario de base et de revenir en arrière. Voici cette technique appliquée à cette fonction.

D'abord le cas de base:

sumInts(6, 5) = 0

Ensuite, l'appel juste au-dessus de cela 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 de cela 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 fonctionsumInts(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 retournent:

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 où ils retournent.

11
OregonTrail

Récursion. En informatique, la récursivité est traitée en détail sous le thème Automates finis.

Dans sa forme la plus simple, il s'agit d'une référence à soi-même. Par exemple, dire que "ma voiture est une voiture" est une déclaration récursive. Le problème est que la déclaration est une récursion infinie dans la mesure où elle ne finira jamais. La définition dans la déclaration d'une "voiture" est qu'il s'agit d'une "voiture" afin qu'elle puisse être remplacée. Cependant, il n'y a pas de fin, car dans le cas d'une substitution, cela devient toujours "ma voiture est une voiture".

Cela pourrait être différent si la déclaration était "Ma voiture est un Bentley. Ma voiture est bleue." Dans ce cas, la substitution dans le second cas de voiture pourrait être "Bentley", ce qui donne "Mon Bentley est bleu". Ces types de substitutions sont expliqués mathématiquement en informatique par le biais de Grammars sans contexte .

La substitution réelle est une règle de production. Etant donné que la déclaration est représentée par S et que wagon est une variable qui peut être un "Bentley", cette déclaration peut être reconstruite de manière récursive.

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

Cela peut être construit de plusieurs manières, chaque | signifiant qu'il y a un choix. S peut être remplacé par l’un de ces choix et S commence toujours vide. Le ε signifie mettre fin à la production. Tout comme S peut être remplacé, il en est de même des autres variables (il n'y en a qu'une et c'est C qui représenterait "bentley").

Donc, en commençant par S étant vide, et en le remplaçant par le premier choix "my"SS devient

"my"S

S peut toujours être substitué car il représente une variable. Nous pourrions choisir "mon" à nouveau, ou ε pour y mettre fin, mais continuons à faire notre déclaration initiale. Nous choisissons l'espace qui signifie que S est remplacé par " "S

"my "S

Suivant permet de choisir C

"my "CS

Et C n'a qu'un seul choix de remplacement

"my bentley"S

Et encore 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" (en remplaçant S par ε termine la production) et nous avons construit récursivement notre déclaration "mon Bentley est bleu".

Pensez à la récursion en tant que 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
5
Travis J

Je vais essayer.

En exécutant l'équation a + sumInts (a + 1, b), je montrerai 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 quantité de temps en années.

Le nombre réel total d'années travaillées avant de prendre sa 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 quiconque, lol. ;-P

5
Bryan

Un très bon conseil que j'ai découvert en apprenant et en comprenant vraiment la récursivité est de passer du temps à apprendre une langue qui n’a aucune forme de construction de boucle autre que la récursion. 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 tutoriel sur les schémas, constitue également une excellente introduction à la conception de programmes en termes d'architecture et de conception.

Mais fondamentalement, vous devez investir du temps. Sans une compréhension "ferme" de la récursion, certains algorithmes, tels que le retour arrière, vous sembleront toujours "difficiles" ou même "magiques". Alors, persévérez. :-RÉ

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

4
Th3Minstr3l

Je pense que la meilleure façon de comprendre les fonctions récursives est de se rendre compte qu'elles sont conçues pour traiter des structures de données récursives. Mais dans votre fonction originale 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)n correspond au nombre de nombres que vous allez ajouter.

SumInts est maintenant récursif sur n, un nombre naturel. Toujours pas une donnée récursive, non? Un nombre naturel peut être considéré comme une structure de données récursive utilisant les axiomes de Peano:

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

Ainsi, 0 = zéro, 1 = successeur (zéro), 2 = successeur (successeur (zéro)), etc.

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 directement la valeur. Pour les cas récursifs vous supposez que la fonction récursive fonctionne déjà et utilisez-la 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 n, nous 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)
    }
}

Maintenant, la fonction récursive est plus simple à programmer. Premièrement, le cas de base, n=0. Que devrions-nous retourner si nous voulons ajouter aucun nombre? La réponse est bien sûr 0. 

Qu'en est-il du cas récursif? Si nous voulons ajouter des nombres n commençant par a et que nous ayons déjà une fonction sumInts fonctionnelle qui fonctionne pour n-1? Eh bien, nous devons ajouter a et ensuite appeler sumInts avec a + 1, nous terminons donc 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écursion sur les données déstructurées. 
4
jdinunzio

Vous pourriez être intéressé par Nisan et Schocken implémentation de fonctions . 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 langage machine virtuelle à langage machine. L'implémentation de fonction proposée est capable de récursion car elle est basée sur une pile.

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

enter image description here

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

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

compiler 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 n place un entier sur cette pile globale.

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

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

256 et 257 sont des adresses de mémoire.

call mult place le numéro de ligne de retour (3) dans 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 il va à l'étiquette function mult. Le code à l'intérieur de mult est 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 returning 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, il se passe ce qui suit:

  • Déposez la dernière valeur de la pile sur l'adresse mémoire du 0ème argument (256 dans ce cas). C’est l’endroit le plus commode pour le dire.
  • Jeter tout ce qui est 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

sub est une fonction primitive du langage de la machine virtuelle. Il prend deux arguments et retourne 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 comment fonctionne la récursion. Pas de magie, juste une pile.

J'ai implémenté votre fonction sumInts 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 au point d'arrêt où lte renvoie 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écompresons" notre récursion. return 0 et allez à la ligne 15 et avancez.

271:  5
272:  0

Ligne 16: add

271:  5

Ligne 17: return 5 et allez à la ligne 15 et avancez.

267:  4
268:  5

Ligne 16: add

267:  9

Ligne 17: return 9 et allez à la ligne 15 et avancez.

263:  3
264:  9

Ligne 16: add

263:  12

Ligne 17: return 12 et allez à la ligne 15 et avancez.

259:  2
260:  12

Ligne 16: add

259:  14

Ligne 17: return 14 et allez à la ligne 21 et avancez.

256:  14

Voilà. Récursion: Glorifié goto.

4
Jackson

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


Les versions précédentes de Google renvoyaient le texte suivant (cité de mémoire):

Récursion

Voir récursion

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

Récursion

Vouliez-vous dire: Récursion


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

3
Pierre Arnaud

Pensez récursion comme un plusieurs clones faire la même chose.

Vous demandez à cloner [1]: "somme les 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 voila !!

3
Christian

Il y a déjà beaucoup de bonnes réponses. Je suis toujours en train d'essayer.
Lorsqu'elle est appelée, une fonction reçoit un espace mémoire alloué, qui est empilée sur le espace mémoire de la fonction appelant. Dans cet espace mémoire, la fonction conserve les paramètres qui lui sont transmis, les variables et leurs valeurs. Ce memory-space disparaît avec l'appel retour final de la fonction. Au fur et à mesure que l'idée de pile disparaît, le memory-space de la fonction appelant devient actif.

Pour les appels récursifs, la même fonction obtient plusieurs memory-space empilés les uns sur les autres. C'est tout. L'idée simple de savoir comment stack fonctionne dans la mémoire d'un ordinateur devrait vous aider à comprendre comment la récursivité se produit lors de la mise en œuvre.

3
Gulshan

Beaucoup des réponses ci-dessus sont très bonnes. Une technique utile pour résoudre la récursivité consiste toutefois à préciser en premier lieu 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

Notez maintenant que ces lignes sont déroutantes (pas fausses, mais déroutantes). 

if (a > b) {
    return 0 
}

Pourquoi le test a>b ?, et pourquoireturn 0

Modifions le code pour mieux refléter l'action d'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)
  }
}

Peut-on le faire encore plus humain comme? Oui! En général, on résume de gauche à droite (2 + 3 + ...). Mais la récurrence ci-dessus se termine de droite à gauche (... + 4 + 5). Changez le code pour le refléter (le - 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 trouveront peut-être que cette fonction est plus déroutante puisque 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écursion). Et encore une fois, la fonction reflète ce que fait un humain (la plupart?): Prend la somme de tous les entiers de gauche et ajoute le «prochain» entier de droite. 

2
user6085241

La récursivité a pris tout son sens lorsque j'ai arrêté de lire ce que les autres en disent ou de le voir comme quelque chose que je peux éviter et que je viens d'écrire du code. J'ai trouvé un problème avec une solution et j'ai essayé de dupliquer la solution sans chercher. J'ai seulement regardé la solution quand je me suis retrouvé coincé, impuissant. Ensuite, j'ai recommencé à essayer de le dupliquer. Je l'ai fait de nouveau sur plusieurs problèmes jusqu'à développer ma propre compréhension et mon sens de la manière d'identifier un problème récursif et de le résoudre. Quand j'ai atteint ce niveau, j'ai commencé à inventer des problèmes et à les résoudre. Cela m'a aidé plus. Parfois, les choses ne peuvent être apprises qu'en les essayant seules et en luttant; jusqu'à ce que vous "l'obteniez". 

2
Phil

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, je trouve cela extrêmement utile, il explique avec la pile et même il explique comment deux récursions fonctionne avec la pile, étape par étape. Je vous recommande d’abord de comprendre le fonctionnement de la pile, ce qui s’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

 enter image description here

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)

 enter image description here  enter image description here

2
user6932350

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

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

si n = 0 alors 1 

alors voyons comment fonctionne la récursivité, je remplace simplement n dans t(n) par n-1 et ainsi de suite. ça a l'air:

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 (n-k) + ... + (n-k-3) + (n-k-2) + (n-k-1) + n;

nous savons que si t(0)=(n-k) est égal à 1 puis n-k=0 alors n=k nous remplaçons k par n:

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

si nous omettons n-n alors:

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

alors 3+2+1+(n-1)+n est un nombre naturel. il 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 le meilleur moyen de comprendre la relation récursive

0