Je comprends presque comment fonctionne la récursion de la queue et la différence entre celle-ci et une récursion normale. Je seulement ne comprends pas pourquoi il ne le fait pas demande à la pile de se souvenir de 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écursion de queue, mais cela n'a aucun sens pour moi.
Le compilateur est simplement capable de transformer cette
int fac_times (int n, int acc) {
if (n == 0) return acc;
else return fac_times(n - 1, acc * n);
}
dans quelque chose comme ça:
int fac_times (int n, int acc) {
label:
if (n == 0) return acc;
acc *= n--;
goto label;
}
Vous demandez pourquoi "il n’est pas nécessaire que stack se souvienne de son adresse de retour".
Je voudrais changer cela. Il utilise utilise la pile pour mémoriser l'adresse de retour. L'astuce est que la fonction dans laquelle la récursion se produit a sa propre adresse de retour sur la pile, et lorsqu'elle saute à la fonction appelée, il traitera cela comme sa propre adresse de retour.
Concrètement, sans optimisation des appels de queue:
f: ...
CALL g
RET
g:
...
RET
Dans ce cas, lorsque g
est appelé, la pile ressemblera à ceci:
SP -> Return address of "g"
Return address of "f"
D'autre part, avec l'optimisation des appels de queue:
f: ...
JUMP g
g:
...
RET
Dans ce cas, lorsque g
est appelé, la pile ressemblera à ceci:
SP -> Return address of "f"
Clairement, lorsque g
reviendra, il retournera à l'emplacement d'où f
a é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.
Les valeurs de retour dans la fonction récursive régulière sont composées de deux types de valeurs:
Regardons votre exemple:
int factorial (int n) {
if (n == 0) return 1;
else return n * factorial(n - 1);
}
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 à s'effondrer, j'ai:
[Stack_f(5): return 5 * [Stack_f(4): 4 * [Stack_f(3): 3 * ... [1[1]]
Notez que chaque pile stocke, outre les valeurs que j'ai mentionnées, toute la portée de la fonction. Donc, 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), il me faut ~ 100k pour calculer factorielle (100), etc.
Dans une récursion de queue, je passe le résultat des calculs partiels dans chaque image récursive à la suivante à l'aide de paramètres. Voyons notre exemple factoriel, queue récursive:
int factorielle (int n) { int helper (int num, int accumulated) { si num == 0 retour accumulé else return helper (num - 1, nombre cumulé *.) } aide au retour (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]]]]
Voir les différences? Dans les appels récursifs "normaux", 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 valeurs les plus anciennes.
La fonction récursive régulière va comme suit:
type regular(n)
base_case
computation
return (result of computation) combined with (regular(n towards base case))
Pour le transformer en une récursion de la queue nous:
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)
Regarde la différence?
Etant donné qu'aucun état n'est stocké sur les piles de cas d'appels non frontaliers, elles ne sont pas si importantes. Certaines langues/interprètes substituent alors l'ancienne pile par la nouvelle. Donc, en l'absence de cadres 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 non.
La récursion de la queue peut généralement être transformée en boucle par le compilateur, en particulier lorsque des accumulateurs sont utilisés.
// tail recursion
int fac_times (int n, int acc = 1) {
if (n == 0) return acc;
else return fac_times(n - 1, acc * n);
}
compiler quelque chose comme
// accumulator
int fac_times (int n) {
int acc = 1;
while (n > 0) {
acc *= n;
n -= 1;
}
return acc;
}
Voici un exemple simple qui montre le fonctionnement des fonctions récursives:
long f (long n)
{
if (n == 0) // have we reached the bottom of the ocean ?
return 0;
// code executed in the descendence
return f(n-1) + 1; // recurrence
// code executed in the ascendence
}
La récursion de la queue est une fonction récursive simple, où la récurrence est effectuée à la fin de la fonction. Aucun code n'est donc généré par ascendance, ce qui aide la plupart des compilateurs de langages de programmation de haut niveau à effectuer ce que l'on appelle Tail Recursion Optimization . , a aussi une optimisation plus complexe connue sous le nom de modulo Tail récursivité
Le compilateur est suffisamment intelligent pour comprendre la récursion finale. Dans ce cas, lors du retour d'un appel récursif, il n'y a aucune opération en attente et l'appel récursif est la dernière instruction. , en supprimant l’implémentation de la pile.Considérons ci-dessous le code.
void tail(int i) {
if(i<=0) return;
else {
system.out.print(i+"");
tail(i-1);
}
}
Après avoir effectué l'optimisation, le code ci-dessus est converti en un code inférieur à celui-ci.
void tail(int i) {
blockToJump:{
if(i<=0) return;
else {
system.out.print(i+"");
i=i-1;
continue blockToJump; //jump to the bolckToJump
}
}
}
Voici comment le compilateur optimise Tail Recursion.
Ma réponse est plutôt une conjecture, parce que la récursivité est quelque chose qui concerne la mise en œuvre interne.
En queue récursive, la fonction récursive est appelée à la fin de la même fonction. Le compilateur peut probablement optimiser de la manière suivante:
Comme vous pouvez le constater, nous finalisons la fonction d'origine avant la prochaine itération de la même fonction. Nous n'utilisons donc pas réellement la pile.
Mais je crois que s’il existe des destructeurs à appeler dans la fonction, cette optimisation risque de ne pas s’appliquer.
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 si n'est pas écrit correctement.
Je vais expliquer à la fois fonction récursive simple et la fonction récursive de queue
Pour écrire une fonction simple récursive
A partir de l'exemple donné:
public static int fact(int n){
if(n <=1)
return 1;
else
return n * fact(n-1);
}
De l'exemple ci-dessus
if(n <=1)
return 1;
Est le facteur décisif quand sortir de la boucle
else
return n * fact(n-1);
Le traitement réel doit-il être effectué?
Laissez-moi la tâche un par un pour faciliter la compréhension.
Voyons ce qui se passe en interne si je lance fact(4)
public static int fact(4){
if(4 <=1)
return 1;
else
return 4 * fact(4-1);
}
La boucle If
échoue et passe donc à la boucle else
so, elle retourne 4 * fact(3)
Dans la mémoire de pile, nous avons 4 * fact(3)
En remplaçant n = 3
public static int fact(3){
if(3 <=1)
return 1;
else
return 3 * fact(3-1);
}
La boucle If
échoue et passe à la boucle else
donc il retourne 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)
En substituant n = 2
public static int fact(2){
if(2 <=1)
return 1;
else
return 2 * fact(2-1);
}
La boucle If
échoue et passe à la boucle else
donc il retourne 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)
En substituant n = 1
public static int fact(1){
if(1 <=1)
return 1;
else
return 1 * fact(1-1);
}
If
loop est vrai
donc il retourne 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 de fait (4) = 4 * 3 * 2 * 1 = 24
Le Tail Recursion serait
public static int fact(x, running_total=1) {
if (x==1) {
return running_total;
} else {
return fact(x-1, running_total*x);
}
}
public static int fact(4, running_total=1) {
if (x==1) {
return running_total;
} else {
return fact(4-1, running_total*4);
}
}
La boucle If
échoue et passe donc à la boucle else
so, elle retourne fact(3, 4)
Dans la mémoire de pile, nous avons fact(3, 4)
En remplaçant n = 3
public static int fact(3, running_total=4) {
if (x==1) {
return running_total;
} else {
return fact(3-1, 4*3);
}
}
La boucle If
échoue et passe à la boucle else
donc il retourne fact(2, 12)
Dans la mémoire de pile, nous avons fact(2, 12)
En substituant n = 2
public static int fact(2, running_total=12) {
if (x==1) {
return running_total;
} else {
return fact(2-1, 12*2);
}
}
La boucle If
échoue et passe à la boucle else
donc il retourne fact(1, 24)
Dans la mémoire de pile, nous avons fact(1, 24)
En substituant n = 1
public static int fact(1, running_total=24) {
if (x==1) {
return running_total;
} else {
return fact(1-1, 24*1);
}
}
If
loop est vrai
donc il retourne running_total
La sortie pour running_total = 24
Enfin, le résultat de fait (4,1) = 24