Je comprends presque comment fonctionne la récursivité de queue et la différence entre elle et une récursivité normale. je ne comprends pas pourquoi il n'a pas besoin de pile de se rappeler son adresse de retour.
// tail recursion
int fac_times (int n, int acc) {
if (n == 0) return acc;
else return fac_times(n - 1, acc * n);
}
int factorial (int n) {
return fac_times (n, 1);
}
// normal recursion
int factorial (int n) {
if (n == 0) return 1;
else return n * factorial(n - 1);
}
Il n'y a rien à faire après avoir appelé une fonction elle-même dans une fonction de récursivité de queue, mais cela n'a aucun sens pour moi.
c
algorithm
recursion
tail-recursion
Alan Coromano
la source
la source
-O3
. Le lien est pour une discussion précédente qui couvre un terrain très similaire et discute de ce qui est nécessaire pour mettre en œuvre cette optimisation.Réponses:
Le compilateur est simplement capable de transformer cela
en quelque chose comme ça:
la source
Vous demandez pourquoi "il n'est pas nécessaire que la pile se souvienne de son adresse de retour".
Je voudrais renverser la situation. Il fait utiliser la pile de se rappeler l'adresse de retour. L'astuce est que la fonction dans laquelle la récursivité de queue se produit a sa propre adresse de retour sur la pile, et lorsqu'elle saute à la fonction appelée, elle la traitera comme sa propre adresse de retour.
Concrètement, sans optimisation des appels de fin:
Dans ce cas, lors de l'
g
appel, la pile ressemblera à:D'autre part, avec l'optimisation des appels de fin:
Dans ce cas, lors de l'
g
appel, la pile ressemblera à:Clairement, lors du
g
retour, il retournera à l'endroit d'où il af
été appelé.EDIT : L'exemple ci-dessus utilise le cas où une fonction appelle une autre fonction. Le mécanisme est identique lorsque la fonction s'appelle elle-même.
la source
La récursivité de queue peut généralement être transformée en boucle par le compilateur, en particulier lorsque des accumulateurs sont utilisés.
compilerait en quelque chose comme
la source
Il y a deux éléments qui doivent être présents dans une fonction récursive:
Une fonction récursive "régulière" garde (2) dans le cadre de la pile.
Les valeurs de retour dans la fonction récursive régulière sont composées de deux types de valeurs:
Regardons votre exemple:
La trame f (5) "stocke" le résultat de son propre calcul (5) et la valeur de f (4), par exemple. Si j'appelle factorial (5), juste avant que les appels de pile ne commencent à se réduire, j'ai:
Notez que chaque pile stocke, en plus des valeurs que j'ai mentionnées, toute la portée de la fonction. Ainsi, l'utilisation de la mémoire pour une fonction récursive f est O (x), où x est le nombre d'appels récursifs que je dois effectuer. Donc, si j'ai besoin de 1 ko de RAM pour calculer factorielle (1) ou factorielle (2), j'ai besoin d'environ 100 ko pour calculer factorielle (100), et ainsi de suite.
Une fonction Tail Recursive met (2) dans ses arguments.
Dans une récursion de queue, je passe le résultat des calculs partiels dans chaque image récursive à la suivante en utilisant des paramètres. Voyons notre exemple factoriel, Tail Recursive:
int factoriel (int n) {int helper (int num, int accumulated) {if num == 0 return accumulated else return helper (num - 1, accumulated * num)} return helper (n, 1)
}
Regardons ses cadres dans factoriel (4):
Vous voyez les différences? Dans les appels récursifs "réguliers", les fonctions de retour composent récursivement la valeur finale. Dans Tail Recursion, ils ne font référence qu'au cas de base (le dernier évalué) . Nous appelons accumulator l'argument qui garde la trace des anciennes valeurs.
Modèles de récursivité
La fonction récursive régulière va comme suit:
Pour le transformer en une récursivité Tail nous:
Regardez:
Regarde la différence?
Optimisation des appels de queue
Puisqu'aucun état n'est stocké sur les piles Non-Border-Cases des piles Tail Call, ils ne sont pas si importants. Certaines langues / interprètes remplacent alors l'ancienne pile par la nouvelle. Ainsi, sans cadres de pile contraignant le nombre d'appels, les appels de queue se comportent comme une boucle for dans ces cas.
C'est à votre compilateur de l'optimiser, ou non.
la source
Voici un exemple simple qui montre comment fonctionnent les fonctions récursives:
La récursivité de la queue est une simple fonction récursive, où la récurrence est effectuée à la fin de la fonction, donc aucun code n'est fait en ascendance, ce qui aide la plupart des compilateurs de langages de programmation de haut niveau à faire ce que l'on appelle l' optimisation de la récursivité de la queue , a également un optimisation plus complexe connue sous le nom de module de récurrence de queue
la source
La fonction récursive est une fonction qui appelle par elle-même
Il permet aux programmeurs d'écrire des programmes efficaces en utilisant une quantité minimale de code .
L'inconvénient est qu'ils peuvent provoquer des boucles infinies et d'autres résultats inattendus s'ils ne sont pas écrits correctement .
Je vais expliquer à la fois la fonction récursive simple et la fonction récursive de queue
Pour écrire une fonction récursive simple
À partir de l'exemple donné:
À partir de l'exemple ci-dessus
Est le facteur décisif pour sortir de la boucle
Le traitement réel doit-il être effectué
Permettez-moi de rompre la tâche une par une pour une meilleure compréhension.
Voyons ce qui se passe en interne si je cours
fact(4)
If
la boucle échoue donc elle va enelse
boucle donc elle retourne4 * fact(3)
Dans la mémoire de pile, nous avons
4 * fact(3)
Remplacer n = 3
If
la boucle échoue donc il va àelse
boucledonc ça revient
3 * fact(2)
Rappelez-vous que nous avons appelé `` 4 * fact (3) ''
La sortie pour
fact(3) = 3 * fact(2)
Jusqu'à présent, la pile a
4 * fact(3) = 4 * 3 * fact(2)
Dans la mémoire de pile, nous avons
4 * 3 * fact(2)
Remplacer n = 2
If
la boucle échoue donc elle passe enelse
boucledonc ça revient
2 * fact(1)
Rappelez-vous que nous avons appelé
4 * 3 * fact(2)
La sortie pour
fact(2) = 2 * fact(1)
Jusqu'à présent, la pile a
4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)
Dans la mémoire de pile, nous avons
4 * 3 * 2 * fact(1)
Remplacer n = 1
If
la boucle est vraiedonc ça revient
1
Rappelez-vous que nous avons appelé
4 * 3 * 2 * fact(1)
La sortie pour
fact(1) = 1
Jusqu'à présent, la pile a
4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1
Enfin, le résultat du fait (4) = 4 * 3 * 2 * 1 = 24
La récursion de la queue serait
If
la boucle échoue donc elle va enelse
boucle donc elle retournefact(3, 4)
Dans la mémoire de pile, nous avons
fact(3, 4)
Remplacer n = 3
If
la boucle échoue donc elle passe enelse
boucledonc ça revient
fact(2, 12)
Dans la mémoire de pile, nous avons
fact(2, 12)
Remplacer n = 2
If
la boucle échoue donc elle passe enelse
boucledonc ça revient
fact(1, 24)
Dans la mémoire de pile, nous avons
fact(1, 24)
Remplacer n = 1
If
la boucle est vraiedonc ça revient
running_total
La sortie pour
running_total = 24
Enfin, le résultat de fait (4,1) = 24
la source
Ma réponse est plutôt une supposition, car la récursivité est liée à l'implémentation interne.
Dans la récursivité de queue, la fonction récursive est appelée à la fin de la même fonction. Le compilateur peut probablement optimiser de la manière ci-dessous:
Comme vous pouvez le voir, nous sommes en train de terminer la fonction d'origine avant la prochaine itération de la même fonction, donc nous n'utilisons pas réellement la pile.
Mais je crois que s'il y a des destructeurs à appeler dans la fonction, cette optimisation peut ne pas s'appliquer.
la source
Le compilateur est suffisamment intelligent pour comprendre la récursivité de la queue. Dans le cas où, lors du retour d'un appel récursif, il n'y a pas d'opération en attente et l'appel récursif est la dernière instruction, tombez dans la catégorie de la récursivité de la queue. Le compilateur effectue essentiellement une optimisation de la récursivité de la queue, en supprimant l'implémentation de la pile.
Après avoir effectué l'optimisation, le code ci-dessus est converti en code ci-dessous.
C'est ainsi que le compilateur effectue l'optimisation de la récurrence de queue.
la source