Il arrive que l’utilisation de la récursivité soit meilleure que celle d’une boucle et que l’utilisation d’une boucle est meilleure que l’utilisation de la récursivité. Choisir le «bon» choix peut économiser des ressources et / ou réduire le nombre de lignes de code.
Existe-t-il des cas où une tâche ne peut être effectuée qu’en utilisant la récursion plutôt qu’une boucle?
INC (r)
,JZDEC (r, z)
peut mettre en œuvre une machine de Turing. Il n'y a pas de 'récursivité' - c'est un saut si zéro sinon DECrement. Si la fonction Ackermann est calculable (elle l’est), cette machine enregistreuse peut le faire.Réponses:
Oui et non. En fin de compte, rien ne permet de calculer la récursivité comme le ferait une boucle, mais la boucle prend beaucoup plus de place. Par conséquent, la seule chose que la récursion puisse faire est que les boucles ne peuvent pas rendre certaines tâches super faciles.
Promenez-vous dans un arbre. Marcher dans un arbre avec la récursion est stupide-facile. C'est la chose la plus naturelle du monde. Marcher dans un arbre avec des boucles est beaucoup moins simple. Vous devez conserver une pile ou une autre structure de données pour garder une trace de ce que vous avez fait.
Souvent, la solution récursive à un problème est plus jolie. C'est un terme technique, et c'est important.
la source
A
qui trouve quelque chose dans un arbre. ChaqueA
fois qu'il rencontre cette chose, il lance une autre fonction récursiveB
qui trouve une chose liée dans le sous-arbre à la position où elle a été lancéeA
. Une foisB
la récursion terminée, celle-ci revientA
et cette dernière poursuit sa propre récursivité. On peut déclarer une pile pourA
et une pourB
, ou la placerB
dans laA
boucle. Si l'on insiste pour utiliser une seule pile, les choses deviennent vraiment compliquées.Therefore, the one thing recursion can do that loops can't is make some tasks super easy.
Et la seule chose que les boucles peuvent faire que la récursivité ne peut pas faire, c'est de rendre certaines tâches super faciles. Avez-vous vu les choses laides et non intuitives que vous devez faire pour transformer les problèmes les plus naturels en itération d'une récurrence naïve en récurrence de la queue afin qu'ils ne fassent pas exploser?map
oufold
(en fait, si vous choisissez de les considérer comme des primitives, je pense que vous pouvez utiliserfold
/ enunfold
tant que troisième alternative aux boucles ou à la récursivité). Sauf si vous écrivez du code de bibliothèque, les cas où vous devriez vous inquiéter de la mise en œuvre de l'itération sont rares, plutôt que la tâche qu'elle est censée accomplir - dans la pratique, cela signifie que les boucles explicites et la récursivité explicite sont également médiocres. des abstractions à éviter au plus haut niveau.Non.
Raffinons les mêmes bases des minimums nécessaires pour calculer, il suffit d'être en mesure de boucler (cela seul ne suffit pas, mais est un élément nécessaire). Peu importe comment .
Tout langage de programmation pouvant implémenter une machine de Turing s'appelle Turing complete . Et il y a beaucoup de langues qui sont très complètes.
Ma langue préférée dans la voie en bas de "cela fonctionne réellement?" La complétude est celle de FRACTRAN , qui est complète . Il a une structure en boucle et vous pouvez y implémenter une machine de Turing. Ainsi, tout ce qui est calculable peut être implémenté dans un langage sans récursivité. Par conséquent, il n'y a rien que la récursivité puisse vous apporter en termes de calculabilité, ce qu'un simple bouclage ne peut pas.
Cela se résume vraiment à quelques points:
Cela ne veut pas dire qu'il existe certaines classes problématiques qu'il est plus facile de penser à la récursion plutôt qu'à la boucle, ou à la boucle plutôt qu'à la récursion. Cependant, ces outils sont également puissants.
Et bien que je porte cela à l'extrême «esolang» (principalement parce que vous pouvez trouver des éléments complets et implémentés de Turing de manière plutôt étrange), cela ne signifie pas pour autant que les esolangs sont facultatifs. Il existe toute une liste de choses qui sont accidentellement terminées par Turing, y compris Magic the Gathering, Sendmail, les modèles MediaWiki et le système de types Scala. Beaucoup d'entre eux sont loin d'être optimaux lorsqu'il s'agit de faire quelque chose de concret, c'est juste que vous pouvez calculer tout ce qui est calculable en utilisant ces outils.
Cette équivalence peut devenir particulièrement intéressante lorsque vous entrez dans un type particulier de récursivité appelé appel final .
Si vous avez, disons, une méthode factorielle écrite comme suit:
Ce type de récursivité sera réécrit sous forme de boucle - aucune pile utilisée. Ces approches sont en effet souvent plus élégantes et plus faciles à comprendre que la boucle équivalente écrite, mais encore une fois, pour chaque appel récursif, il peut exister une boucle équivalente et pour chaque boucle, un appel récursif est écrit.
Il arrive aussi que la conversion de la boucle simple en appel final soit récursive et plus difficile à comprendre.
Si vous voulez entrer dans la théorie, consultez la thèse de Church Turing . Vous pouvez également trouver utile la thèse de l' église-turing sur CS.SE.
la source
Vous pouvez toujours transformer un algorithme récursif en une boucle, qui utilise une structure de données Last-In-First-Out (pile AKA) pour stocker un état temporaire, car l'appel récursif est exactement ce qu'il est, stocker l'état actuel dans une pile puis plus tard restaurer l'état. La réponse est si courte: non, il n’ya pas de tels cas .
Cependant, un argument peut être avancé pour "oui". Prenons un exemple concret et simple: le tri par fusion. Vous devez diviser les données en deux parties, fusionner, trier les parties, puis les combiner. Même si vous ne faites pas un appel de fonction de langage de programmation pour fusionner le tri afin de le faire sur les pièces, vous devez implémenter une fonctionnalité identique à celle utilisée pour effectuer un appel de fonction (envoi de l'état à votre propre pile, saut vers début de boucle avec différents paramètres de départ, puis enlevez plus tard l’état de votre pile).
S'agit-il d'une récursion, si vous implémentez l'appel vous-même, en tant qu'étapes séparées "push state" et "jump to begin" et "pop state"? Et la réponse à cette question est la suivante: non, ce n’est toujours pas appelé récursivité, c’est une itération avec pile explicite (si vous souhaitez utiliser la terminologie établie).
Notez que cela dépend aussi de la définition de "tâche". Si la tâche est à trier, vous pouvez le faire avec de nombreux algorithmes, dont beaucoup n’ont pas besoin de récursion. Si tâche consiste à implémenter un algorithme spécifique, tel que le tri par fusion, l'ambiguïté ci-dessus s'applique.
Examinons donc la question: existe-t-il des tâches générales pour lesquelles il n’existe que des algorithmes de type récursivité? De commentaire de @WizardOfMenlo sous la question, la fonction Ackermann est un exemple simple de cela. Ainsi, le concept de récursivité est autonome, même s'il peut être implémenté avec une construction de programme informatique différente (itération avec pile explicite).
la source
Cela dépend de la rigueur avec laquelle vous définissez "récursivité".
Si nous exigeons strictement que la pile d'appels soit impliquée (ou quel que soit le mécanisme utilisé pour maintenir l'état du programme), nous pouvons toujours le remplacer par quelque chose qui ne le fait pas. En effet, les langages qui conduisent naturellement à un usage intensif de la récursivité ont tendance à avoir des compilateurs qui font un usage intensif de l'optimisation de l'appel final, de sorte que ce que vous écrivez est récursif mais que vous exécutez est itératif.
Mais prenons le cas où nous effectuons un appel récursif et utilisons le résultat d'un appel récursif pour cet appel récursif.
Rendre le premier appel récursif itératif est facile:
Nous pouvons ensuite nettoyer enlever le
goto
pour éviter les vélociraptors et l'ombre de Dijkstra:Mais pour supprimer les autres appels récursifs, nous allons devoir stocker les valeurs de certains appels dans une pile:
Maintenant, quand nous considérons le code source, nous avons certainement transformé notre méthode récursive en une méthode itérative.
Compte tenu de ce qui a été compilé, nous avons transformé le code qui utilise la pile d'appels pour implémenter la récursivité dans le code qui ne fonctionne pas (et ce faisant, nous avons transformé le code qui lève une exception de débordement de pile pour des valeurs même très petites dans un code qui se contente de prendre un temps pour revenir longtemps atrocement [voir Comment puis - je empêcher ma fonction Ackerman de déborder la pile? quelques autres qui font optimisations le retourner en fait pour beaucoup plus entrées possibles]).
Considérant la manière dont la récursivité est généralement implémentée, nous avons transformé le code qui utilise la pile d'appels en code utilisant une pile différente pour contenir les opérations en attente. Nous pourrions donc affirmer qu'il est toujours récursif, à ce niveau bas.
Et à ce niveau, il n'y a en effet pas d'autre moyen de le contourner. Donc, si vous considérez que cette méthode est récursive, il y a en effet des choses que nous ne pouvons pas faire sans. Généralement, nous n'indiquons pas ce code récursif. Le terme récursion est utile car il recouvre un certain ensemble d’approches et nous permet d’en parler, et nous n’utilisons plus l’une d’elles.
Bien sûr, tout cela suppose que vous avez le choix. Il existe des langues interdisant les appels récursifs et des langues dépourvues des structures de boucle nécessaires à l'itération.
la source
La réponse classique est "non", mais permettez-moi d'expliquer pourquoi je pense que "oui" est une meilleure réponse.
Avant de continuer, passons à autre chose: du point de vue de la calculabilité et de la complexité:
Ok, maintenant, mettons un pied dans la pratique, gardons l'autre pied dans la théorie.
La pile d'appels est une structure de contrôle , tandis qu'une pile manuelle est une structure de données . Contrôle et données ne sont pas des concepts égaux, mais ils sont équivalents en ce sens qu'ils peuvent être réduits l'un à l'autre (ou "émulés" l'un par l'autre) du point de vue de la calculabilité ou de la complexité.
Quand cette distinction pourrait-elle avoir de l'importance? Lorsque vous travaillez avec des outils du monde réel. Voici un exemple:
Supposons que vous implémentez N-way
mergesort
. Vous pouvez avoir unefor
boucle qui parcourt chacun desN
segments, les appellemergesort
séparément, puis fusionne les résultats.Comment pourriez-vous paralléliser cela avec OpenMP?
Dans le domaine récursif, c'est extrêmement simple: il suffit de placer
#pragma omp parallel for
votre boucle qui va de 1 à N et vous avez terminé. Dans le domaine itératif, vous ne pouvez pas faire cela. Vous devez créer manuellement des threads et leur transmettre manuellement les données appropriées afin qu'ils sachent quoi faire.D'autre part, il existe d'autres outils (tels que les vectoriseurs automatiques, par exemple
#pragma vector
) qui fonctionnent avec des boucles mais qui sont totalement inutiles avec une récursion.Le fait que les deux paradigmes soient équivalents sur le plan mathématique ne signifie pas qu’ils soient égaux dans la pratique. Un problème qui pourrait être banal à automatiser dans un paradigme (disons, la parallélisation de boucle) pourrait être beaucoup plus difficile à résoudre dans l'autre paradigme.
C'est-à-dire que les outils d'un paradigme ne se traduisent pas automatiquement par d'autres paradigmes.
Par conséquent, si vous avez besoin d'un outil pour résoudre un problème, il est probable qu'il ne fonctionnera qu'avec un type d'approche particulier. Par conséquent, vous ne pourrez pas résoudre le problème avec une approche différente, même si vous pouvez prouver mathématiquement que le problème peut être résolu de toute façon.
la source
Laissant de côté le raisonnement théorique, examinons la récursivité et les boucles du point de vue de la machine (matérielle ou virtuelle). La récursivité est une combinaison de flux de contrôle qui permet de démarrer l’exécution de certains codes et de revenir à l’achèvement (dans une vue simpliste lorsque les signaux et les exceptions sont ignorés) et de données transmises à cet autre code (arguments) et renvoyées par le (résultat). Habituellement, aucune gestion de mémoire explicite n’est impliquée. Cependant, il existe une allocation implicite de mémoire de pile pour enregistrer les adresses de retour, les arguments, les résultats et les données locales intermédiaires.
Une boucle est une combinaison de flux de contrôle et de données locales. En comparant cela à la récursivité, nous pouvons constater que la quantité de données dans ce cas est fixe. Le seul moyen de dépasser cette limitation consiste à utiliser une mémoire dynamique (également appelée tas ) pouvant être allouée (et libérée) à tout moment.
Résumer:
En supposant que la partie flux de contrôle soit raisonnablement puissante, la seule différence réside dans les types de mémoire disponibles. Il nous reste donc 4 cas (le pouvoir d’expressivité est indiqué entre parenthèses):
Si les règles du jeu sont un peu plus strictes et que l'implémentation récursive n'est pas autorisée à utiliser des boucles, nous obtenons plutôt ceci:
La principale différence avec le scénario précédent est que le manque de mémoire de pile ne permet pas à la récursivité sans boucles de faire plus d’étapes lors de l’exécution qu’il n’ya de lignes de code.
la source
Oui. Il existe plusieurs tâches courantes faciles à accomplir à l'aide de la récursivité, mais impossibles avec de simples boucles:
la source
Il y a une différence entre les fonctions récursives et les fonctions récursives primitives. Les fonctions récursives primitives sont celles qui sont calculées à l'aide de boucles, où le nombre maximal d'itérations de chaque boucle est calculé avant le début de l'exécution de la boucle. (Et "récursif" ici n'a rien à voir avec l'utilisation de la récursivité).
Les fonctions récursives primitives sont strictement moins puissantes que les fonctions récursives. Vous obtiendriez le même résultat si vous utilisiez des fonctions qui utilisent la récursivité, pour lesquelles la profondeur maximale de la récursivité doit être calculée à l’avance.
la source
Si vous programmez en c ++ et utilisez c ++ 11, il y a une chose à faire à l'aide des récurrences: les fonctions constexpr. Mais la norme limite cela à 512, comme expliqué dans cette réponse . L'utilisation de boucles dans ce cas n'est pas possible, car dans ce cas, la fonction ne peut pas être constexpr, mais elle est modifiée dans c ++ 14.
la source
la source
Je suis d'accord avec les autres questions. Vous ne pouvez rien faire avec la récursion avec une boucle.
MAIS , à mon avis, la récursivité peut être très dangereuse. Premièrement, pour certains, il est plus difficile de comprendre ce qui se passe réellement dans le code. Deuxièmement, au moins pour C ++ (Java je ne suis pas sûr), chaque étape de la récursion a un impact sur la mémoire car chaque appel de méthode provoque une accumulation de mémoire et l'initialisation de l'en-tête de méthodes. De cette façon, vous pouvez faire exploser votre pile. Essayez simplement la récursion des nombres de Fibonacci avec une valeur d'entrée élevée.
la source