Qu'est-ce que la queue de la récursivité?
Tout en commençant à apprendre lisp, j'ai rencontré le terme récursives terminales. Que veut dire exactement?
- Pour les curieux: à la fois tout et tout ont été, dans la langue pour un temps très long. Tout était en usage dans le Vieil anglais; tandis que est un Moyen anglais de développement de tout. Comme les conjonctions, ils sont interchangeables dans ce sens, mais tout n'a pas survécu dans l'anglais Américain standard.
- Peut-être que c'est la fin, mais c'est un très bon article sur la queue récursive:programmerinterview.com/index.php/recursion/tail-recursion
- Un des grands avantages de l'identification d'une queue-fonction récursive, c'est qu'il peut être converti en un processus itératif de forme et donc de revivre l'algorithme de la méthode de la pile-dessus. Pourriez visiter la réponse de @Kyle Cronin et quelques autres ci-dessous
- Ce lien de @yesudeep est le meilleur, le plus détaillé de la description que j'ai trouvé: lua.org/pil/6.3.html
Vous devez vous connecter pour publier un commentaire.
Envisager une fonction simple qui ajoute les N premiers nombres entiers. (par exemple,
sum(5) = 1 + 2 + 3 + 4 + 5 = 15
).Ici est un simple JavaScript de mise en œuvre qui utilise la récursivité:
Si vous l'avez appelé
recsum(5)
, c'est ce que l'interpréteur JavaScript permettrait d'évaluer:Remarquez comment chaque appel récursif est à effectuer avant de l'interpréteur JavaScript commence à réellement faire le travail de calcul de la somme.
Voici une queue-une version récursive de la même fonction:
Voici la séquence des événements qui allaient se produire si vous avez appelé
tailrecsum(5)
, (ce qui serait effectivementtailrecsum(5, 0)
, en raison de la valeur par défaut second argument).Dans la queue-récursive cas, chaque évaluation de l'appel récursif, la
running_total
est mis à jour.Remarque: La réponse a utilisé des exemples de Python. Celles-ci ont été modifiés pour JavaScript, depuis interpréteurs Python ne prennent pas en charge la queue d'appel d'optimisation. Cependant, alors que la queue d'appel d'optimisation est une partie de l'ECMAScript 2015 specs, la plupart JavaScript interprètes ne le supportent pas.
tail recursion
peut être réalisé dans une langue qui n'optimisent pas loin de la queue des appels.else
est mieux depuis l'ensemble de laif
est un queue expression.def sum(x):
sum=0
while x: (sum, x) = (sum+x, x=1)
return sum
- voir?Dans traditionnel de la récursivité, le modèle typique est que vous effectuez vos appels récursifs d'abord, et puis vous prenez la valeur de retour de l'appel récursif, et de calculer le résultat. De cette manière, vous n'obtenez pas le résultat de votre calcul jusqu'à ce que vous avez retourné à partir de chaque appel récursif.
Dans queue de récursivité, vous effectuez vos calculs en premier, et puis vous exécutez l'appel récursif, en passant les résultats de votre étape en cours de la prochaine étape récursive. Cette résultats dans la dernière instruction étant en forme de
(return (recursive-function params))
. Fondamentalement, la valeur de retour de tout récursive étape est la même que la valeur de retour de la prochaine appel récursif.La conséquence de cela est que une fois que vous êtes prêt à effectuer votre prochaine étape récursive, vous n'avez pas besoin de la pile en cours de cadre plus. Cela permet de l'optimisation. En fait, avec un convenablement écrit compilateur, vous ne devriez jamais avoir un débordement de pile ricaner avec une queue appel récursif. Simplement réutiliser la pile en cours pour la prochaine étape récursive. Je suis sûr que Lisp ne ce.
Un point important est que la queue de la récursivité est essentiellement équivalent à une boucle. Ce n'est pas juste une question d'optimisation du compilateur, mais un fait fondamental de l'expressivité. Cela va dans les deux sens: vous pouvez prendre une boucle de la forme
où
E
etQ
sont des expressions etS
est une séquence d'instructions, et de le transformer en une queue de fonction récursiveBien sûr,
E
,S
, etQ
doivent être définies pour calculer certains intéressant de valeur sur certaines variables. Par exemple, la boucle de la fonctionest équivalent à la queue-fonction récursive(s)
(Cet "habillage" de la queue-fonction récursive avec une fonction avec moins de paramètres est une commune fonctionnelle idiom.)
else { return k; }
peut être modifié àreturn k;
Cet extrait du livre de la Programmation en Lua montre comment faire une queue de récursivité (en Lua, mais il devrait s'appliquer à Lisp trop) et pourquoi c'est mieux.
Donc, vous voyez, quand vous faites un appel récursif comme:
Ce n'est pas la queue récursive parce que vous avez encore des choses à faire (ajouter 1) dans cette fonction après l'appel récursif est faite. Si vous entrez un nombre très élevé, il ne sera probablement provoquer un débordement de pile.
L'utilisation de la récursivité, chaque appel récursif pousse une autre entrée sur la pile d'appel. Lorsque la récursivité est terminée, l'application doit éclater chaque entrée off tout le chemin du retour vers le bas.
Avec la queue de la récursivité, en fonction de la langue le compilateur peut être en mesure à l'effondrement de la pile vers le bas à une seule entrée, de sorte que vous économiser de l'espace de pile...Un grand requête récursive peut effectivement provoquer un débordement de pile.
Fondamentalement Queue récurrences peuvent être optimisées dans l'itération.
Au lieu d'expliquer avec des mots, voici un exemple. C'est un Schéma de la version de la fonction factorielle:
Ici est une version de factorielle qui est de la queue-récursive:
Vous remarquerez que dans la première version de l'appel récursif de fait est introduit dans la multiplication de l'expression, et donc l'état doit être sauvegardé sur la pile lors de la prise de l'appel récursif. Dans la queue, une version récursive il n'y a pas d'autre S-expression d'attente pour la valeur de l'appel récursif, et puisqu'il n'y est pas plus de travail à faire, l'état n'a pas à être sauvegardés sur la pile. En règle générale, le Régime fonctions récursives terminales à l'utilisation constante de l'espace de pile.
list-reverse
procédure sera exécutée de la constante d'espace de pile mais à créer et à développer une structure de données sur le tas. Un parcours d'arbres pourraient utiliser une simulation de la pile, dans un argument supplémentaire. etc.Le jargon file a ceci à dire à propos de la définition de la queue de la récursivité:
queue de récursivité /n./
Si vous n'êtes pas malade déjà, voir la queue de la récursivité.
Queue de récursivité se réfère à l'appel récursif est la dernière dans le dernier logique de l'instruction dans l'algorithme récursif.
Généralement dans la récursivité, vous avez un cas de base qui est-ce qui empêche les appels récursifs et commence à éclater la pile d'appel. Pour utiliser un exemple classique, bien que plus C-ish que Lisp, la fonction factorielle illustre la queue de la récursivité. L'appel récursif se produit après la vérification de la base de cas d'état.
L'appel initial factorielle serait
factorial(n)
oùfac=1
(valeur par défaut) et n est le nombre pour lequel la factorielle est d'être calculé.else
est l'étape que l'on pourrait appeler un "cas de base", mais s'étend sur plusieurs lignes. Suis-je malentendu vous ou est mon hypothèse est correcte? La queue de la récursivité est seulement bon pour l'un des paquebots?factorial
exemple est juste le classique exemple simple, c'est tout.Cela signifie que, plutôt que d'avoir à pousser le pointeur d'instruction sur la pile, vous pouvez simplement sauter du haut d'une fonction récursive, et de continuer l'exécution. Cela permet pour les fonctions de répéter indéfiniment sans débordement de la pile.
J'ai écrit un blog post sur le sujet, qui a graphique des exemples de ce que la pile d'images ressemblent.
Voici un petit extrait de code de la comparaison de deux fonctions. La première est traditionnelle de la récursivité pour trouver la factorielle d'un nombre donné. La seconde utilise la récursivité tail.
Très simple et intuitif à comprendre.
Un moyen facile de savoir si une fonction récursive est une queue récursive est si elle renvoie une valeur concrète dans le cas de base. Ce qui signifie qu'il ne retourne pas 1 ou true ou quelque chose comme ça. Il est plus que probable retour une variante de l'un des paramètres de la méthode.
Une autre façon est-à-dire si l'appel récursif est libre de tout ajout, l'arithmétique, de modification, etc... ce qui Signifie que son rien, mais un pur appel récursif.
La meilleure façon pour moi de comprendre
tail call recursion
est un cas particulier de la récursivité où la dernier appel(ou de la queue) est la fonction elle-même.En comparant les exemples fournis en Python:
^RÉCURSIVITÉ
^RÉCURSION SUR LA QUEUE
Comme vous pouvez le voir en général une version récursive, le dernier appel dans le bloc de code est
x + recsum(x - 1)
. Donc, après l'appel de larecsum
méthode, il y a une autre opération qui estx + ..
.Cependant, dans la queue, une version récursive, le dernier appel(ou la queue d'appel) dans le bloc de code est
tailrecsum(x - 1, running_total + x)
ce qui signifie que le dernier appel à la méthode elle-même et aucune opération après que.Ce point est important parce que la queue de la récursivité comme on le voit ici n'est pas de faire de la mémoire de grandir, car lorsque le sous-jacent VM voit un appel de fonction dans une queue de position (la dernière expression évaluée dans une fonction), il élimine l'actuel cadre de la pile, qui est connu comme la Queue d'Appel d'Optimisation(TCO).
MODIFIER
NB. Ne garder à l'esprit que l'exemple ci-dessus est écrit en Python, dont l'exécution ne prend pas en charge le coût de possession. C'est juste un exemple pour expliquer le point. TCO est pris en charge dans les langues comme le Régime, Haskell etc
En Java, voici un éventuel queue récursive de la mise en œuvre de la fonction de Fibonacci:
Cela contraste avec la norme récursive de mise en œuvre:
iter
àacc
quanditer < (n-1)
.Je ne suis pas un programmeur Lisp, mais je pense que cette aidera.
En gros, c'est un style de programmation tels que l'appel récursif est la dernière chose que vous faites.
Ici est un Common Lisp exemple qui ne les factorielles à l'aide de la queue de la récursivité. En raison de la pile, moins de la nature, on pourrait effectuer incroyablement grande factorielle des calculs ...
Et puis pour le plaisir, vous pouvez essayer de
(format nil "~R" (! 25))
En bref, une queue de récursivité a l'appel récursif comme le dernier déclaration de la fonction, afin de ne pas avoir à attendre l'appel récursif.
Donc, c'est une queue de récursivité c'est à dire N(x - 1, p * x) est la dernière instruction de la fonction où le compilateur est intelligent pour comprendre qu'il peut être optimisé grâce à une boucle for (factorielle). Le deuxième paramètre p porte l'intermédiaire de la valeur du produit.
C'est le non-récursives terminales de l'écriture au-dessus de la fonction factorielle (bien que certains compilateurs C++ peuvent être en mesure d'optimiser toute façon).
mais ce n'est pas:
J'ai écrit un long post intitulé "La Compréhension De La Queue De La Récursivité – Visual Studio C++ – Vue D'Assemblage"
ici est un Perl version 5 de la
tailrecsum
fonction mentionné plus tôt.Ceci est un extrait de La Structure et l'Interprétation des Programmes d'Ordinateur sur la queue de la récursivité.
Envisager le problème du calcul de la factorielle d'un nombre.
Une approche simple serait:
Supposons que vous appelez factorielle(4). La récursivité de l'arbre serait:
Le maximum de profondeur de récursion dans le cas ci-dessus est O(n).
Cependant, considérons l'exemple suivant:
La récursivité de l'arbre pour factTail(4) serait:
Ici aussi, le maximum de la profondeur de récursion est O(n), mais aucun des appels ajoute une variable supplémentaire à la pile. Donc le compilateur peut faire disparaître une pile.
La fonction récursive est une fonction qui appels par lui-même
Elle permet aux programmeurs d'écrire des programmes efficaces à l'aide d'un quantité minimale de code.
L'inconvénient est qu'ils peuvent provoquer une boucle infinie et autres résultats inattendus si pas écrit correctement.
Je vais vous expliquer les deux Simple fonction Récursive et la Queue fonction Récursive
Pour écrire un Simple fonction récursive
de la boucle qui est le cas de la boucle
À partir de l'exemple donné:
À partir de l'exemple ci-dessus
Est le facteur décisif au moment de quitter la boucle
Est le véritable traitement à faire
Permettez-moi de le découper les tâches une par une, pour faciliter la compréhension.
Nous allons voir ce qui se passe en interne, si je lance
fact(4)
If
boucle échoue donc, il va àelse
boucleon en revient donc
4 * fact(3)
Dans la pile mémoire, nous avons
4 * fact(3)
La substitution de n=3
If
boucle échoue donc, il va àelse
boucleon en revient donc
3 * fact(2)
Me souviens 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 pile mémoire, nous avons
4 * 3 * fact(2)
La substitution de n=2
If
boucle échoue donc, il va àelse
boucleon en revient donc
2 * fact(1)
Me souviens 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 pile mémoire, nous avons
4 * 3 * 2 * fact(1)
Substituant n=1
If
boucle est vraion en revient donc
1
Me souviens 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
La Queue de Récursivité serait
If
boucle échoue donc, il va àelse
boucleon en revient donc
fact(3, 4)
Dans la pile mémoire, nous avons
fact(3, 4)
La substitution de n=3
If
boucle échoue donc, il va àelse
boucleon en revient donc
fact(2, 12)
Dans la pile mémoire, nous avons
fact(2, 12)
La substitution de n=2
If
boucle échoue donc, il va àelse
boucleon en revient donc
fact(1, 24)
Dans la pile mémoire, nous avons
fact(1, 24)
Substituant n=1
If
boucle est vraion en revient donc
running_total
La sortie pour
running_total = 24
Enfin, le résultat de fait(4,1) = 24
Queue de la récursivité est la vie que vous vivez actuellement. Constamment à vous recycler la même trame de pile, encore et encore, car il n'y a pas de raison ou de moyens pour le retour à un "précédent" cadre". Le passé est fini et fait avec de sorte qu'il peut être mis au rebut. Vous obtenez une image, à jamais, à l'avenir, jusqu'à ce que votre processus inévitablement meurt.
L'analogie décompose lorsque vous envisagez de certains processus peuvent utiliser des images supplémentaires, mais sont toujours considérés comme des queue-récursive si la pile n'est pas croître à l'infini.
Comprendre certaines différences fondamentales qui existent entre la queue-appel de la récursivité et de la non-queue-appelons récursivité, nous pouvons explorer la .NET implémentations de ces techniques.
Voici un article avec quelques exemples en C#, F# et C++\CLI: Aventures dans la Queue de la Récursivité en C#, F# et C++\CLI.
C# ne pas optimiser pour la queue-appelons récursivité alors que F# ne.
Les différences de principe impliquer boucles vs Lambda calcul. C# est conçu avec des boucles à l'esprit alors que F# est construit à partir des principes de Lambda calcul. Pour un très bon (et gratuit) livre sur les principes de Lambda calcul, voir La Structure et l'Interprétation des Programmes d'Ordinateur, par Abelson, Sussman, et Sussman.
Concernant les appels tail en F#, pour un très bon article d'introduction, voir Introduction détaillée à la Queue des Appels en F#. Enfin, voici un article qui couvre la différence entre les non-récursion sur la queue et la queue de l'appel de la récursivité (en F#): Tail-recursion et non la queue de la récursivité en fa dièse.
Si vous voulez lire sur certaines des différences de conception de la queue-appelons récursivité entre C# et F#, voir Générer la Queue-Appel Opcode en C# et F#.
Si vous vous souciez assez pour voulez savoir quelles sont les conditions d'empêcher le compilateur C# à partir de l'exécution de la queue-appel d'optimisation, voir cet article: JIT CLR queue-appel conditions.
Un queue récursive fonction est une fonction récursive où la dernière opération avant le retour, c'est de faire la fonction récursive appel. Qui est, la valeur de retour de la fonction récursive appel est immédiatement retourné. Par exemple, votre code devrait ressembler à ceci:
Compilateurs et interpréteurs de mettre en œuvre queue appeler l'optimisation ou queue appel élimination pouvez optimiser récursive code pour éviter les débordements de pile. Si votre compilateur ou l'interpréteur ne pas mettre en œuvre la queue d'appel d'optimisation (comme la Disponible interprète) il n'y a aucun avantage supplémentaire à l'écriture de votre code de cette façon.
Par exemple, c'est un standard de la fonction factorielle récursive en Python:
Et c'est un appel tail une version récursive de la fonction factorielle:
(Notez que même si c'est du code Python, le Disponible interprète ne fait pas la queue d'appel d'optimisation, de sorte que l'organisation de votre code comme celui-ci ne confère aucun d'exécution des prestations.)
Vous pourriez avoir à rendre votre code un peu plus illisible de faire usage de la queue d'appel d'optimisation, comme le montre la factorielle exemple. (Par exemple, le cas de base est maintenant un peu peu intuitive, et la
accumulator
paramètre est utilisé efficacement comme une sorte de variable globale.)Mais l'avantage de la queue d'appel d'optimisation est qu'il empêche les erreurs de dépassement de pile. (Je note que vous pouvez obtenir ce même résultat en utilisant un algorithme itératif au lieu d'un récursive.)
Débordements de pile sont causées lorsque la pile d'appel a eu trop d'objets d'image poussé sur. Une image de l'objet est poussé sur la pile d'appel lorsqu'une fonction est appelée, et a sauté hors de la pile d'appel lorsque la fonction retourne. Objets d'image contenant des informations telles que les variables locales et quelle ligne de code pour revenir à quand la fonction retourne.
Si votre fonction récursive fait trop d'appels récursifs sans retour, la pile d'appel peut dépasser son objet frame limite. (Le nombre varie selon la plate-forme; en Python il est de 1000 objets d'image par défaut.) Cela provoque une débordement de pile erreur. (Hey, c'est là que le nom de ce site web vient d'!)
Toutefois, si la dernière chose que votre fonction récursive n'est de faire de l'appel récursif et renvoie sa valeur de retour, alors il n'y a aucune raison qu'il a besoin de garder l'image actuelle de l'objet doit rester sur la pile d'appel. Après tout, si il n'y a pas de code après l'appel de fonction récursive, il n'y a pas de raison de s'accrocher à l'image actuelle de l'objet de variables locales. Donc, nous pouvons nous débarrasser de l'image actuelle de l'objet immédiatement plutôt que de le garder sur la pile d'appel. Le résultat final est que votre pile d'appel de ne pas grandir en taille, et ne peut donc pas de débordement de pile.
Un compilateur ou l'interpréteur doit avoir la queue d'appel d'optimisation d'une fonction pour qu'il soit capable de reconnaître quand la queue d'appel d'optimisation peut être appliquée. Même alors, vous pouvez avoir réorganiser le code de votre fonction récursive de faire usage de la queue d'appel d'optimisation, et c'est à vous, si cette diminution potentielle de la lisibilité est la valeur de l'optimisation.
Il existe deux types fondamentaux de récurrences: tête de la récursivité et queue de récursivité.
Prises de cette super awesome post.
Veuillez considérer le lire.
La récursivité moyen d'un appel de fonction elle-même. Par exemple:
Tail-Recursion signifie la récursivité que conclure de la fonction:
Voir, la dernière chose que l'onu-clos de la fonction (procédure, dans le Schéma de jargon), c'est d'appeler lui-même. L'autre (plus utile) exemple:
À l'aide de la procédure, la DERNIÈRE chose qu'il fait, si la gauche n'est pas nul, est d'appeler lui-même (APRÈS les contre quelque chose et cdr quelque chose). C'est fondamentalement la façon dont vous carte une liste.
La queue-la récursivité est un grand avantage que l'interprète (ou le compilateur, dépend de la langue et le vendeur) peut l'optimiser et de le transformer en quelque chose d'équivalent à une boucle while. Comme question de fait, dans le Schéma de la tradition, la plupart des "pour" et "boucle" while est fait en une queue-de récursivité manière (il n'y est pas et tout, autant que je sache).
Queue de Récursivité est assez rapide par rapport à la normale de la récursivité.
Il est rapide, car la sortie des ancêtres appel ne sera pas écrit dans la pile de garder la piste.
Mais la récursivité tous l'ancêtre des appels de sortie de l'écrit dans la pile de garder la piste.
Cette question a beaucoup de bonnes réponses... mais je ne peux pas aider mais carillon avec une autre de prendre sur la façon de définir la "queue de la récursivité", ou au moins "bonne queue de récursivité." À savoir: doit-on regarder comme une propriété d'une expression donnée dans un programme? Ou doit-on regarder comme une propriété d'un la mise en œuvre d'un langage de programmation?
Pour en savoir plus sur le dernier point de vue, il est un classique papier par la Volonté Clinger, "la Bonne Queue de Récursivité et de l'Efficacité de l'Espace" (PLDI 1998), qui a défini "la bonne queue de récursivité" en tant que propriété d'un langage de programmation de mise en œuvre. La définition est construite de façon à permettre d'ignorer les détails de mise en œuvre (comme par exemple si la pile d'appel est en fait représenté par l'exécution de la pile ou par l'intermédiaire d'un segment de mémoire allouée lié liste d'images).
Pour ce faire, il utilise l'analyse asymptotique: pas de temps d'exécution du programme que l'on voit habituellement, mais plutôt de programme l'utilisation de l'espace. De cette façon, l'utilisation de l'espace d'un segment de mémoire allouée liste liée vs un moteur d'exécution de la pile des appels finit par être asymptotiquement équivalent; donc, on en vient à ignorer que le langage de programmation de mise en œuvre détail (un détail qui a certainement des questions tout à fait un peu de pratique, mais il peut brouiller les pistes tout à fait un peu lorsque l'on tente de déterminer si une œuvre est de satisfaire à l'exigence d'être "la propriété de la queue récursive")
Le papier est intéressant de l'étude minutieuse d'un certain nombre de raisons:
Il donne une définition inductive de la queue expressions et queue appels d'un programme. (Une telle définition, et pourquoi de tels appels sont importants, semble être l'objet de la plupart des autres réponses ici.)
Voici ces définitions, juste pour donner une saveur du texte:
(une queue appel récursif, ou comme le dit le rapport, "l'auto-appel tail" est un cas particulier d'une queue d'appel où la procédure est appelée elle-même.)
Il fournit des définitions pour six différents "machines" pour l'évaluation du Schéma de Base, où chaque machine a le même comportement observable sauf pour la asymptotique de l'espace classe de complexité que chacun est en.
Par exemple, après avoir donné les définitions pour les machines avec, respectivement, 1. basée sur la pile de gestion de la mémoire, 2. la collecte des ordures, mais pas de queue appels, 3. la collecte des ordures et de la queue des appels, le papier continue de l'avant avec encore plus de stockage de pointe stratégies de gestion, tels que le 4. "evlis queue de récursivité", où l'environnement n'a pas besoin d'être conservées dans l'évaluation de la dernière sous-argument d'expression dans une queue d'appel, 5. la réduction de l'environnement d'une fermeture à juste les variables libres de fermeture, et 6. soi-disant "sécurité-pour-l'espace" sémantique tel que défini par D'Appel et de Shao.
Afin de prouver que les machines appartiennent en fait à six distincts de l'espace de la complexité des classes, le papier, pour chaque paire de machines à sous de comparaison, fournit des exemples concrets de programmes qui permettront d'exposer asymptotique de l'espace blowup sur une seule machine, mais pas les autres.
(Lire par-dessus ma réponse maintenant, je ne sais pas si je suis parvenu à réellement capturer les points cruciaux de la Clinger papier. Mais, hélas, je ne peux pas consacrer plus de temps à l'élaboration de cette réponse à l'heure actuelle.)
Beaucoup de gens ont déjà expliqué récursion. Je voudrais citer quelques réflexions à propos de quelques avantages que la récursivité donne de l'ouvrage “la Simultanéité dans .NET, les tendances Modernes de la simultanées et de la programmation parallèle” par Riccardo Terrell:
Voici également quelques notes intéressantes à partir du même livre sur la queue de la récursivité: