web-dev-qa-db-fra.com

Comment fonctionne exactement la récursion de la queue?

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.

116
Alan Coromano

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;
}
165
Alexey Frunze

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.

55
Lindydancer

Il y a deux éléments qui doivent être présents dans une fonction récursive:

  1. L'appel récursif
  2. Un endroit pour garder le compte des valeurs de retour.

Une fonction récursive "régulière" maintient (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:

  • Autres valeurs de retour
  • Résultat du calcul de la fonction propre

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.

Une fonction Tail Recursive put (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 à 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.

Modèles de récursivité

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:

  • Introduire une fonction d'assistance qui porte l'accumulateur
  • exécuter la fonction d'assistance à l'intérieur de la fonction principale, avec l'accumulateur défini 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)

Regarde la différence? 

Optimisation d'appel de queue

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.

11
Lucas Ribeiro

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;
}
11
mepcotterell

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é

6
Khaled.K

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.

0

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:

  1. Laisser la fonction en cours s’enrouler (c’est-à-dire que la pile utilisée est rappelée)
  2. Stocker les variables qui vont être utilisées comme arguments de la fonction dans un stockage temporaire 
  3. Après cela, appelez à nouveau la fonction avec l’argument temporairement enregistré.

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.

0
iammilind

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 

  1. Le premier point à considérer est le moment où vous décidez de sortir De la boucle qui est la boucle if
  2. La seconde est quel processus faire si nous sommes notre propre fonction

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)

  1. En substituant n = 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 elseso, elle retourne 4 * fact(3)

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

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

  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

 enter image description here

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

  1. En substituant n = 4 
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 elseso, elle retourne fact(3, 4)

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

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

  1. 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

 enter image description here

0
Nursnaaz