Il y a deux éléments qui doivent être présents dans une fonction récursive :
- L'appel récursif
- Un endroit pour garder le compte des valeurs de retour.
Une fonction récursive "régulière" garde (2) dans le cadre de la pile.
Les valeurs de retour dans une fonction récursive régulière sont composées de deux types de valeurs :
- Autres valeurs de retour
- Résultat du calcul de la fonction propriétaire
Reprenons votre exemple :
int factorial (int n) {
if (n == 0) return 1;
else return n * factorial(n - 1);
}
Le cadre 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 la pile ne commencent à s'effondrer, j'ai.. :
[Stack_f(5): return 5 * [Stack_f(4): 4 * [Stack_f(3): 3 * ... [1[1]]
Remarquez 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 faire. Donc, si j'ai besoin de 1kb de RAM pour calculer la factorielle(1) ou la factorielle(2), j'ai besoin de ~100k pour calculer la factorielle(100), et ainsi de suite.
Une fonction récursive de queue met (2) dans ses arguments.
Dans une récursion de queue, je passe le résultat des calculs partiels dans chaque cadre récursif au suivant en utilisant des paramètres. Voyons notre exemple de factorielle, Tail Recursive :
int factorial (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 factorial(4) :
[Stack f(4, 5): Stack f(3, 20): [Stack f(2,60): [Stack f(1, 120): 120]]]]
Vous voyez les différences ? Dans les appels récursifs "ordinaires", les fonctions de retour composent récursivement la valeur finale. Dans la récursion de queue, elles ne font référence qu'au cas de base (le dernier évalué). . Nous appelons accumulateur l'argument qui garde la trace des anciennes valeurs.
Modèles de récursion
La fonction récursive régulière est la suivante :
type regular(n)
base_case
computation
return (result of computation) combined with (regular(n towards base case))
Pour le transformer en une récursion Tail nous :
- Introduire une fonction d'aide qui transporte l'accumulateur.
- exécuter la fonction d'aide à l'intérieur de la fonction principale, avec l'accumulateur réglé sur le cas de base.
Regardez :
type tail(n):
type helper(n, accumulator):
if n == base case
return accumulator
computation
accumulator = computation combined with accumulator
return helper(n towards base case, accumulator)
helper(n, base case)
Vous voyez la différence ?
Optimisation de l'appel de queue
Puisqu'aucun état n'est stocké sur les cas non frontaliers des piles Tail Call, ils ne sont pas si importants. Certains langages/interprètes remplacent alors l'ancienne pile par la nouvelle. Ainsi, sans trame de pile limitant 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 pas.
17 votes
Récursion de la queue est récursion "normale". Cela signifie seulement que la récursion se produit à la fin de la fonction.
7 votes
... Mais on peut l'implémenter d'une manière différente au niveau de l'IL que la récursion normale, en réduisant la profondeur de la pile.
2 votes
BTW, gcc peut effectuer l'élimination de la récursion de queue sur l'exemple "normal" ici.
0 votes
En référence à mon commentaire ci-dessus, voir stackoverflow.com/a/3679409/2509 .
0 votes
@KeithS Qu'est-ce que le "niveau IL" ?
0 votes
Lire Lambda : le GOTO de l'Ultimage l'un des documents originaux de Scheme.
1 votes
@Geek - Je suis un développeur C#, donc mon "langage d'assemblage" est MSIL ou simplement IL. Pour C/C++, remplacez IL par ASM.
0 votes
@dmckee Je ne vois rien dans cette référence qui dit gcc est en effectuant une sorte de réécriture de code récursif sans queue en code récursif avec queue.
0 votes
Je pense que nous devrions être clairs sur les termes : ce que le PO demande est optimisation de la récursion de la queue . La récursion de queue peut être optimisée ou non, et c'est l'optimisation qui élimine le besoin d'un cadre de pile supplémentaire.
1 votes
@ShannonSeverance J'ai découvert que gcc le fait par le simple fait d'examiner le code d'assemblage émis avec sans
-O3
. Le lien renvoie à une discussion antérieure qui couvre un terrain très similaire et traite de ce qui est nécessaire pour mettre en œuvre cette optimisation.