Existe-t-il un problème de performance si nous utilisons une boucle au lieu de la récursion ou inversement dans des algorithmes où les deux peuvent servir le même objectif? Ex: Vérifiez si la chaîne donnée est un palindrome. J'ai vu beaucoup de programmeurs utiliser la récursivité pour montrer quand un simple algorithme d'itération peut convenir. Le compilateur joue-t-il un rôle vital dans la décision de l’utilisation?
Il est possible que la récursivité soit plus coûteuse, selon que la fonction récursive est queue récursive (la dernière ligne est un appel récursif). La récursion de queue devrait être reconnue par le compilateur et optimisée par rapport à son homologue itératif (tout en maintenant l'implémentation claire et concise que vous avez dans votre code).
J'écrirais l'algorithme de la manière la plus logique et la plus claire pour le pauvre meunier (que ce soit vous-même ou quelqu'un d'autre) qui doit maintenir le code dans quelques mois ou quelques années. Si vous rencontrez des problèmes de performances, profilez votre code, puis examinez uniquement l'optimisation en passant à une implémentation itérative. Vous voudrez peut-être examiner mémoization et programmation dynamique .
Les boucles peuvent améliorer les performances de votre programme. La récursivité peut permettre à votre programmeur de gagner en performances. Choisissez ce qui est le plus important dans votre situation!
Comparer récursivité à itération revient à comparer un tournevis cruciforme à un tournevis à tête plate. Dans la plupart des cas, vous pourriez enlever toute vis à tête plate à empreinte cruciforme, mais ce serait plus simple si vous utilisiez le tournevis conçu pour cette vis, non?
Certains algorithmes se prêtent simplement à la récursivité en raison de la façon dont ils ont été conçus (séquences de Fibonacci, traversée d'une structure arborescente, etc.). La récursivité rend l'algorithme plus succinct et plus facile à comprendre (donc partageable et réutilisable).
En outre, certains algorithmes récursifs utilisent "Lazy Evaluation", ce qui les rend plus efficaces que leurs frères itératifs. Cela signifie qu'ils ne font les calculs coûteux qu'au moment où ils sont nécessaires plutôt qu'à chaque exécution de la boucle.
Cela devrait être suffisant pour vous aider à démarrer. Je vais chercher des articles et des exemples pour vous aussi.
Lien 1: Haskel vs PHP (Récursivité vs Itération)
Voici un exemple où le programmeur devait traiter un grand ensemble de données en utilisant PHP. Il montre à quel point il était facile de traiter avec Haskel en utilisant la récursivité, mais comme PHP ne disposait d'aucun moyen simple pour appliquer la même méthode, il a été obligé d'utiliser l'itération pour obtenir le résultat.
http://blog.webspecies.co.uk/2011-05-31/lazy-evaluation-with-php.html
Lien 2: Maîtrise de la récursivité
La mauvaise réputation de la récursion provient en grande partie des coûts élevés et de l'inefficacité des langages impératifs. L'auteur de cet article explique comment optimiser les algorithmes récursifs pour les rendre plus rapides et plus efficaces. Il explique également comment convertir une boucle traditionnelle en une fonction récursive et les avantages de l'utilisation de la récursivité en bout de chaîne. Ses derniers mots résument vraiment certains de mes points clés:
"La programmation récursive offre au programmeur une meilleure façon d’organiser le code de manière à la fois maintenable et logiquement cohérente."
Lien 3: La récursivité est-elle toujours plus rapide que la boucle? (Réponse)
Voici un lien vers une réponse à une question similaire à la vôtre. L'auteur souligne que bon nombre des points de repère associés à la récurrence ou à la mise en boucle sont très spécifiques au langage. Les langages impératifs sont généralement plus rapides avec une boucle et plus lents avec la récursivité et inversement pour les langages fonctionnels. Je suppose que le principal point à retenir de ce lien est qu’il est très difficile de répondre à la question dans un sens agnostique/aveugle de la langue.
La récursivité est plus coûteuse en mémoire, car chaque appel récursif nécessite généralement de placer une adresse mémoire sur la pile - de sorte que le programme puisse ultérieurement revenir à ce point.
Néanmoins, il existe de nombreux cas dans lesquels la récursivité est beaucoup plus naturelle et lisible que les boucles, comme lorsque vous travaillez avec des arbres. Dans ces cas, je vous recommanderais de vous en tenir à la récursivité.
Généralement, on s’attend à ce que la pénalité de performance se situe dans l’autre sens. Les appels récursifs peuvent conduire à la construction de trames de pile supplémentaires. la pénalité pour cela varie. De plus, dans certains langages tels que Python (plus correctement, dans certaines implémentations de certains langages ...), vous pouvez vous heurter assez facilement aux limites de pile pour les tâches que vous pourriez spécifier de manière récursive, telles que la recherche de la valeur maximale dans une structure de données arborescente. Dans ces cas, vous voulez vraiment vous en tenir à des boucles.
Écrire de bonnes fonctions récursives peut réduire quelque peu la pénalité de performance, en supposant que vous disposiez d'un compilateur qui optimise les récursions de la queue, etc. (Vérifiez également que la fonction est vraiment récursive - beaucoup de gens font des erreurs sur.)
Outre les cas "Edge" (calcul haute performance, très grande profondeur de récursivité, etc.), il est préférable d'adopter l'approche qui exprime le mieux votre intention, qui est bien conçue et que vous pouvez maintenir. Optimiser uniquement après avoir identifié un besoin.
La récursivité est meilleure que l'itération pour les problèmes qui peuvent être décomposés en multiple, des morceaux plus petits.
Par exemple, pour créer un algorithme de Fibonnaci récursif, vous décomposez fib (n) en fib (n-1) et fib (n-2) et calculez les deux parties. L'itération vous permet seulement de répéter une fonction encore et encore.
Cependant, Fibonacci est en réalité un exemple fragmenté et je pense que l'itération est en réalité plus efficace. Notez que fib (n) = fib (n-1) + fib (n-2) et fib (n-1) = fib (n-2) + fib (n-3). fib (n-1) est calculé deux fois!
Un meilleur exemple est un algorithme récursif pour un arbre. Le problème de l’analyse du noeud parent peut être décomposé en multiple plus petits problèmes d’analyse de chaque noeud enfant. Contrairement à l'exemple de Fibonacci, les plus petits problèmes sont indépendants les uns des autres.
Alors oui, la récursivité est préférable à l'itération pour les problèmes qui peuvent être décomposés en plusieurs problèmes plus petits, indépendants et similaires.
Lorsque vous utilisez la récursivité, vos performances se détériorent, car appeler une méthode, dans n’importe quelle langue, nécessite une préparation importante: le code appelant affiche une adresse de retour, des paramètres d’appel, certaines autres informations de contexte telles que les registres du processeur peuvent être sauvegardées quelque part, et lors du retour, la la méthode appelée poste une valeur de retour qui est ensuite récupérée par l'appelant et toutes les informations de contexte précédemment enregistrées sont restaurées. la diff de performance entre une approche itérative et une approche récursive réside dans le temps que ces opérations prennent.
D'un point de vue implémentation, vous commencez vraiment à remarquer la différence lorsque le temps nécessaire pour gérer le contexte d'appel est comparable au temps nécessaire à l'exécution de votre méthode. Si votre méthode récursive prend plus de temps à exécuter alors la partie de gestion du contexte de l'appelant, procédez de manière récursive, car le code est généralement plus lisible et facile à comprendre et vous ne remarquerez pas la perte de performances. Sinon, allez de manière itérative pour des raisons d'efficacité.
Je crois que la récursion de queue dans Java n'est pas optimisée pour le moment. Les détails sont éparpillés partout this discussion sur LtU et les liens associés. Cela peut figurer dans la version 7 à venir, mais apparemment, il présente certaines difficultés lorsqu'il est combiné à l'inspection de pile, car certaines images seraient manquantes. Stack Inspection est utilisé pour implémenter leur modèle de sécurité à grain fin depuis Java 2.
Dans de nombreux cas, la récursivité est plus rapide en raison de la mise en cache, ce qui améliore les performances. Par exemple, voici une version itérative du tri par fusion utilisant la routine de fusion traditionnelle. Il fonctionnera plus lentement que l'implémentation récursive en raison de la mise en cache des performances améliorées.
public static void sort(Comparable[] a)
{
int N = a.length;
aux = new Comparable[N];
for (int sz = 1; sz < N; sz = sz+sz)
for (int lo = 0; lo < N-sz; lo += sz+sz)
merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
}
private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi)
{
if (hi <= lo) return;
int mid = lo + (hi - lo) / 2;
sort(a, aux, lo, mid);
sort(a, aux, mid+1, hi);
merge(a, aux, lo, mid, hi);
}
PS - C’est ce que le professeur Kevin Wayne (Université de Princeton) a expliqué lors du cours sur les algorithmes présenté sur Coursera.
Il existe de nombreux cas où cela donne une solution beaucoup plus élégante que la méthode itérative, l'exemple courant étant la traversée d'un arbre binaire, il n'est donc pas nécessairement plus difficile à maintenir. En général, les versions itératives sont généralement un peu plus rapides (et lors de l'optimisation peuvent bien remplacer une version récursive), mais les versions récursives sont plus simples à comprendre et à mettre en œuvre correctement.
La récursivité est très utile dans certaines situations. Par exemple, considérons le code pour trouver la factorielle
int factorial ( int input )
{
int x, fact = 1;
for ( x = input; x > 1; x--)
fact *= x;
return fact;
}
Maintenant considérons-le en utilisant la fonction récursive
int factorial ( int input )
{
if (input == 0)
{
return 1;
}
return input * factorial(input - 1);
}
En observant ces deux choses, nous pouvons voir que la récursion est facile à comprendre. Mais si vous ne l'utilisez pas avec précaution, vous risquez également d'être sujet à des erreurs. Supposons que si nous manquons if (input == 0)
, le code sera exécuté pendant un certain temps et se terminera généralement par un débordement de pile.
Cela dépend de la langue. En Java, vous devez utiliser des boucles. Les langages fonctionnels optimisent la récursivité.
En utilisant la récursivité, vous payez un appel de fonction à chaque "itération", alors qu'avec une boucle, vous ne payez généralement qu'une incrémentation/décrémentation. Ainsi, si le code de la boucle n'est pas beaucoup plus compliqué que celui de la solution récursive, la boucle sera généralement supérieure à la récursion.
La récursivité et l'itération dépendent de la logique métier que vous souhaitez implémenter, même si dans la plupart des cas, elle peut être utilisée de manière interchangeable. La plupart des développeurs optent pour la récursivité car il est plus facile à comprendre.
La récursivité est plus simple (et donc plus fondamentale) que toute définition possible d'une itération. Vous pouvez définir un système complet de Turing avec seulement un paire de combinateurs (oui, même une récursion est une notion dérivée dans un tel système). Lambda le calcul est un système fondamental tout aussi puissant, comportant des fonctions récursives. Mais si vous voulez définir correctement une itération, vous aurez besoin de beaucoup plus de primitives pour commencer.
En ce qui concerne le code - non, le code récursif est en fait beaucoup plus facile à comprendre et à maintenir qu'un code purement itératif, car la plupart des structures de données sont récursives. Bien sûr, pour réussir, il faudrait au moins un langage prenant en charge les fonctions d'ordre élevé et les fermetures, du moins - pour obtenir tous les combinateurs et itérateurs standard. En C++, bien entendu, les solutions récursives complexes peuvent paraître un peu laides, à moins que vous ne soyez un utilisateur assidu de FC++ et du même type.
Si vous parcourez simplement une liste, assurez-vous de le parcourir.
Quelques autres réponses ont mentionné (en premier lieu) la traversée des arbres. C'est vraiment un très bon exemple, car c'est une chose très courante à faire avec une structure de données très commune. La récursivité est extrêmement intuitive pour ce problème.
Découvrez les méthodes "trouver" ici: http://penguin.ewu.edu/cscd300/Topic/BSTintro/index.html
Je penserais qu'en récurrence (non-tail), il y aurait un impact sur les performances pour allouer une nouvelle pile, etc. à chaque appel de la fonction (en fonction de la langue, bien sûr).
En C++, si la fonction récursive est basée sur un modèle, le compilateur a plus de chances de l’optimiser car toutes les déductions de types et instanciations de fonctions se produiront au moment de la compilation. Les compilateurs modernes peuvent également intégrer la fonction si possible. Ainsi, si l'on utilise des indicateurs d'optimisation tels que -O3
ou -O2
dans g++
, les récursions risquent alors d'être plus rapides que les itérations. Dans les codes itératifs, le compilateur a moins de chances de l’optimiser, car il est déjà dans un état plus ou moins optimal (s’il est suffisamment écrit).
Dans mon cas, j’essayais de mettre en œuvre l’exponentiation matricielle en effectuant une quadrature à l’aide d’objets matriciels Armadillo, de manière récursive et itérative. L'algorithme peut être trouvé ici ... https://en.wikipedia.org/wiki/Exponentiation_by_squaring . Mes fonctions ont été modélisées et j'ai calculé les matrices 1,000,000
12x12
élevées à la puissance 10
. J'ai eu le résultat suivant:
iterative + optimisation flag -O3 -> 2.79.. sec
recursive + optimisation flag -O3 -> 1.32.. sec
iterative + No-optimisation flag -> 2.83.. sec
recursive + No-optimisation flag -> 4.15.. sec
Ces résultats ont été obtenus avec gcc-4.8 avec l'indicateur c ++ 11 (-std=c++11
) et Armadillo 6.1 avec Intel mkl. Le compilateur Intel affiche également des résultats similaires.
Récursion? Où puis-je commencer, wiki vous dira "C’est le processus de répétition des éléments de la même manière"
À l'époque où je faisais du C, la récursion C++ était un don divin, comme "Récursion de la queue". Vous découvrirez également que de nombreux algorithmes de tri utilisent la récursivité. Exemple de tri rapide: http://alienryderflex.com/quicksort/
La récursivité est comme tout autre algorithme utile pour un problème spécifique. Peut-être que vous ne trouverez pas d’utilisation immédiate ou fréquente, mais qu’il y aura un problème, vous serez heureux de la disponibilité.
cela dépend de la "profondeur de récursivité". cela dépend de combien la surcharge de l'appel de fonction va influencer le temps total d'exécution.
Par exemple, calculer la factorielle classique de manière récursive est très inefficace en raison de: - risque de débordement de données - risque de débordement de pile - la surcharge de l'appel de fonction occupe 80% du temps d'exécution
tout en développant un algorithme min-max pour l'analyse de la position dans le jeu d'échecs qui analysera les N mouvements suivants peut être implémenté en récurrence sur la "profondeur d'analyse" (comme je le fais ^ _ ^)
Mike a raison. La récursion de la queue est pas optimisée par le compilateur Java ou la machine virtuelle Java. Vous obtiendrez toujours un dépassement de pile avec quelque chose comme ceci:
int count(int i) {
return i >= 100000000 ? i : count(i+1);
}
La récursivité présente l'inconvénient que l'algorithme que vous écrivez à l'aide de la récursion présente une complexité d'espace O(n). Bien que les approches itératives aient une complexité d’espace de O (1), c’est l’avantage d’utiliser l’itération par rapport à la récursion. Alors pourquoi utilisons-nous la récursivité?
Voir ci-dessous.
Parfois, il est plus facile d'écrire un algorithme en utilisant la récursivité alors qu'il est légèrement plus difficile d'écrire le même algorithme en utilisant l'itération. Dans ce cas, si vous choisissez de suivre l'approche par itération, vous devrez gérer vous-même la pile.
N'oubliez pas qu'en utilisant une récursion trop profonde, vous rencontrerez un débordement de pile, en fonction de la taille de pile autorisée. Pour éviter cela, veillez à fournir un scénario de base mettant fin à la récursivité.
Autant que je sache, Perl n'optimise pas les appels en queue, mais vous pouvez les simuler.
sub f{
my($l,$r) = @_;
if( $l >= $r ){
return $l;
} else {
# return f( $l+1, $r );
@_ = ( $l+1, $r );
goto &f;
}
}
Lorsqu'il est appelé pour la première fois, il allouera de l'espace sur la pile. Ensuite, il va changer ses arguments et redémarrer le sous-programme, sans rien ajouter de plus à la pile. Il va donc prétendre qu'il ne s'est jamais appelé lui-même, le transformant en un processus itératif.
Notez qu'il n'y a pas "my @_;
" ou "local @_;
", si vous le faisiez, cela ne fonctionnerait plus.
Si les itérations sont atomiques et que les ordres de grandeur sont plus coûteux que de pousser un nouveau cadre de pile et créer un nouveau fil et vous avez plusieurs cœurs et votre L’environnement d’exécution peut tous les utiliser, puis une approche récursive pourrait améliorer considérablement les performances lorsqu’elle est combinée au multithreading. Si le nombre moyen d'itérations n'est pas prévisible, il peut être judicieux d'utiliser un pool de threads qui contrôlera l'allocation des threads et empêchera votre processus de créer trop de threads et de monopoliser le système.
Par exemple, dans certaines langues, il existe des implémentations de tri par fusion multithread récursives.
Mais encore une fois, le multithreading peut être utilisé avec la boucle plutôt que la récursivité, donc le succès de cette combinaison dépend de plusieurs facteurs, notamment du système d'exploitation et de son mécanisme d'allocation de threads.
En utilisant seulement Chrome 45.0.2454.85 m, la récursion semble être plus rapide.
Voici le code:
(function recursionVsForLoop(global) {
"use strict";
// Perf test
function perfTest() {}
perfTest.prototype.do = function(ns, fn) {
console.time(ns);
fn();
console.timeEnd(ns);
};
// Recursion method
(function recur() {
var count = 0;
global.recurFn = function recurFn(fn, cycles) {
fn();
count = count + 1;
if (count !== cycles) recurFn(fn, cycles);
};
})();
// Looped method
function loopFn(fn, cycles) {
for (var i = 0; i < cycles; i++) {
fn();
}
}
// Tests
var curTest = new perfTest(),
testsToRun = 100;
curTest.do('recursion', function() {
recurFn(function() {
console.log('a recur run.');
}, testsToRun);
});
curTest.do('loop', function() {
loopFn(function() {
console.log('a loop run.');
}, testsToRun);
});
})(window);
RÉSULTATS
// 100 exécutions utilisant la boucle standard
100x pour la boucle. Temps pour compléter: 7.683ms
// 100 exécutions utilisant une approche récursive fonctionnelle avec récursion de queue
100x récursion. Temps pour compléter: 4.841ms
Dans la capture d'écran ci-dessous, la récursivité gagne à nouveau avec une marge plus importante lorsqu'elle est exécutée à 300 cycles par test