Je suis confronté à un problème où la récursivité et l'utilisation d'une boucle semblent être des solutions naturelles. Existe-t-il une convention ou une "méthode préférée" pour des cas comme celui-ci? (Évidemment, ce n'est pas aussi simple que ci-dessous)
Item Search(string desired, Scope scope) {
foreach(Item item in scope.items)
if(item.name == desired)
return item;
return scope.Parent ? Search(desired, scope.Parent) : null;
}
Item Search(string desired, Scope scope) {
for(Scope cur = scope; cur != null; cur = cur.Parent)
foreach(Item item in cur.items)
if(item.name == desired)
return item;
return null;
}
Je privilégie les solutions récursives lorsque:
L'implémentation de la récursivité est beaucoup plus simple que la solution itérative, généralement parce qu'elle exploite un aspect structurel du problème d'une manière que l'approche itérative ne peut pas
Je peux être raisonnablement assuré que la profondeur de la récursivité ne provoquera pas un débordement de pile, en supposant que nous parlons d'un langage qui implémente la récursivité de cette façon
La condition 1 ne semble pas être le cas ici. La solution itérative est à peu près du même niveau de complexité, donc je m'en tiendrai à la route itérative.
Si les performances sont importantes, alors comparez les deux et choisissez sur une base rationnelle. Si ce n'est pas le cas, choisissez en fonction de la complexité, en vous préoccupant d'un éventuel débordement de pile.
Il y a une ligne directrice du livre classique Les éléments du style de programmation (par Kernighan et Plauger) que l'algorithme devrait suivre la structure des données. Autrement dit, les structures récursives sont souvent traitées plus clairement avec des algorithmes récursifs.
La récursivité est utilisée pour exprimer un algorithme qui est naturellement récursif sous une forme qui est plus facilement compréhensible. Un algorithme "naturellement récursif" est un algorithme où la réponse est construite à partir des réponses à des sous-problèmes plus petits qui sont à leur tour construits à partir des réponses à des sous-problèmes encore plus petits, etc. Par exemple, calculer une factorielle.
Dans un langage de programmation qui n'est pas fonctionnel, une approche itérative est presque toujours plus rapide et plus efficace qu'une approche récursive, donc la raison d'utiliser la récursivité est la clarté, pas la vitesse. Si une implémentation récursive finit par être moins claire qu'une implémentation itérative, alors évitez-la par tous les moyens.
Dans ce cas particulier, je jugerais la mise en œuvre itérative plus claire.
Si vous utilisez un langage fonctionnel (ne semble pas l'être), optez pour la récursivité. Sinon, la boucle sera probablement mieux comprise par toute autre personne travaillant sur le projet. Bien sûr, certaines tâches (comme la recherche récursive d'un répertoire) sont mieux adaptées à la récursivité que d'autres.
De plus, si le code ne peut pas être optimisé pour la récursivité d'extrémité, la boucle est plus sûre.
Utilisez la boucle. Il est plus facile à lire et à comprendre (lire du code est toujours beaucoup plus difficile que de l'écrire) et est généralement beaucoup plus rapide.
Il est prouvable que tous les algorithmes récursifs de queue peuvent être déroulés dans une boucle, et vice versa. D'une manière générale, une implémentation récursive d'un algorithme récursif est plus claire à suivre pour le programmeur que l'implémentation de boucle, et est également plus facile à déboguer. De manière générale, les performances réelles de l'implémentation de la boucle seront plus rapides, car une branche/saut dans une boucle est généralement plus rapide à exécuter que de pousser et de faire sauter une trame de pile.
Personnellement, pour les algorithmes récursifs de queue, je préfère m'en tenir à l'implémentation récursive dans toutes les situations sauf les plus gourmandes en performances.
Je préfère les boucles comme
J'utilise des piles (schéma LIFO) pour faire fonctionner les boucles
En Java, les piles sont couvertes par l'interface Deque
// Get all the writable folders under one folder
// Java-like pseudocode
void searchWritableDirs(Folder rootFolder){
List<Folder> response = new List<Folder>(); // Results
Deque<Folder> folderDeque = new Deque<Folder>(); // Stack with elements to inspect
folderDeque.add(rootFolder);
while( ! folderDeque.isEmpty()){
Folder actual = folder.pop(); // Get last element
if (actual.isWritable()) response.add(actual); // Add to response
for(Folder actualSubfolder: actual.getSubFolder()) {
// Here we iterate subfolders, with this recursion is not needed
folderDeque.Push(actualSubfolder);
}
}
log("Folders " + response.size());
}
Moins compliqué, plus compact que
// Get all the writable folders under one folder
// Java-like pseudocode
void searchWritableDirs(Folder rootFolder){
List<Folder> response = new List<Folder>(); // Results
rec_searchWritableDirs(actualSubFolder,response);
log("Folders " + response.size());
}
private void rec_searchWritableDirs(Folder actual,List<Folder> response) {
if (actual.isWritable()) response.add(actual); // Add to response
for(Folder actualSubfolder: actual.getSubFolder()) {
// Here we iterate subfolders, recursion is needed
rec_searchWritableDirs(actualSubFolder,response);
}
}
Ce dernier a moins de code, mais deux fonctions et il est plus difficile de comprendre le code, à mon humble avis.
Je dirais que la version récursive est mieux compréhensible, mais seulement avec des commentaires:
Item Search(string desired, Scope scope) {
// search local items
foreach(Item item in scope.items)
if(item.name == desired)
return item;
// also search parent
return scope.Parent ? Search(desired, scope.Parent) : null;
}
Il est beaucoup plus facile d'expliquer cette version. Essayez d'écrire un joli commentaire sur la version en boucle et vous verrez.
Je trouve la récursivité plus naturelle, mais vous pouvez être obligé d'utiliser la boucle si votre compilateur ne fait pas d'optimisation des appels de queue et que votre arborescence/liste est trop profonde pour la taille de la pile.
Je préfère généralement l'utilisation de boucles. La plupart des bons OOP designs vous permettront d'utiliser des boucles sans avoir à utiliser la récursivité (et donc arrêter le programme de pousser tous ces paramètres et adresses désagréables dans la pile).
Il a plus d'une utilité dans le code procédural où il semble plus logique de penser de manière récursive (car vous ne pouvez pas facilement stocker l'état ou les métadonnées (informations?) Et donc vous créez plus de situations qui mériteraient son utilisation).
La récursivité est bonne pour prototyper une fonction et/ou écrire une base, mais une fois que vous savez que le code fonctionne et que vous y revenez pendant la phase d'optimisation, essayez de le remplacer par une boucle.
Encore une fois, tout cela est subjectif. Choisissez ce qui vous convient le mieux.
Eh bien, j'ai vu des tonnes de réponses et même accepté la réponse, mais je n'ai jamais vu la bonne et je me demandais pourquoi ...
Longue histoire courte :
Évitez toujours les récursions si vous pouvez faire la même unité à produire par des boucles !
Comment fonctionne la récursivité?
• La trame dans la mémoire de pile est allouée pour un seul appel de fonction
• Le cadre contient une référence à la méthode réelle
• Si la méthode a des objets, les objets sont placés dans la mémoire du tas et Frame contiendra une référence à ces objets dans la mémoire du tas.
• Ces étapes sont effectuées pour chaque appel de méthode unique!
Risques:
• StackOverFlow lorsque la pile n'a pas de mémoire pour mettre de nouvelles méthodes récursives.
• OutOfMemory lorsque le tas n'a pas de mémoire pour placer des objets stockés récursifs.
Comment fonctionne la boucle?
• Toutes les étapes précédentes, sauf que l'exécution répétée de code à l'intérieur de la boucle ne consommera pas d'autres données si elles sont déjà consommées.
Risques:
• Le risque unique est à l'intérieur de la boucle alors que votre condition n'existera tout simplement jamais ... Eh bien, cela ne provoquera aucun plantage ou autre, il ne quittera simplement pas la boucle si vous faites naïvement while(true)
:)
Test:
Procédez ensuite dans votre logiciel:
private Integer someFunction(){
return someFunction();
}
Vous obtiendrez StackOverFlow
exception dans une seconde et peut-être OutOfMemory
aussi
Faites ensuite:
while(true){
}
Le logiciel va juste se figer et aucun plantage ne se produira:
Last but not least - for
boucles:
Allez toujours avec les boucles for
parce que telle ou telle façon cette boucle vous oblige quelque peu à donner le point de rupture au-delà duquel la boucle ne va pas, vous pouvez sûrement être super en colère et juste trouver un moyen de faire for
loop ne s'arrête jamais mais je vous conseille de toujours utiliser des boucles au lieu de récursivité pour la gestion de la mémoire et une meilleure productivité pour votre logiciel ce qui est un gros problème de nos jours.
Références:
Vous pouvez également écrire la boucle dans un format plus lisible. La for(init;while;increment)
de C présente quelques inconvénients de lisibilité puisque la commande increment
est mentionnée au début mais exécutée à la fin de la boucle.
VOS DEUX ÉCHANTILLONS NE SONT PAS ÉQUIVALENTS . L'exemple récursif échouera et la boucle non, si vous l'appelez comme: Search(null,null)
. Cela rend la version en boucle meilleure pour moi.
Voici les exemples modifiés (et en supposant que null est faux)
Item Search(string desired, Scope scope) {
if (!scope) return null
foreach(Item item in scope.items)
if(item.name == desired)
return item;
//search parent (recursive)
return Search(desired, scope.Parent);
}
Item Search(string desired, Scope scope) {
// start
Scope cur = scope;
while(cur) {
foreach(Item item in cur.items)
if(item.name == desired)
return item;
//search parent
cur = cur.Parent;
} //loop
return null;
}
Si votre code est compilé, cela fera probablement peu de différence. Faites des tests et voyez combien de mémoire est utilisée et à quelle vitesse elle s'exécute.
Si le système sur lequel vous travaillez a une petite pile (systèmes embarqués), la profondeur de récursivité serait limitée, donc le choix de l'algorithme basé sur la boucle serait souhaitable.