web-dev-qa-db-fra.com

Récursivité vs boucles

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)

Récursivité

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

Loop

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;
}
46
zildjohn01

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.

55
John Feminella

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.

27
RBerteig

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.

18
Tyler McHenry

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.

9
Ed S.

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.

7
Emil H

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.

4
Not Sure

Je préfère les boucles comme

  • La récursivité est sujette aux erreurs
  • Tout le code reste dans une seule fonction/méthode
  • Économies de mémoire et de vitesse

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.

4
user898384

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.

3
ypnos

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.

2
Svante

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.

1
user865927

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:

allocation de mémoire basée sur la pile

1
Android Developer

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)

Récursivité (fixe et appel de queue optimisable)

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

Loop

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;
}
0
Lucio M. Tato

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.

0
Jay

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.

0
switchmode