La récursion est-elle plus rapide que la boucle?

je sais que la récursion est parfois beaucoup plus propre que la boucle, et je ne pose pas de questions sur quand je devrais utiliser la récursion plutôt que l'itération, je sais qu'il ya beaucoup de questions à ce sujet déjà.

ce que je demande, est-ce que la récursion est plus rapide qu'une boucle? Pour moi, il semble, sera toujours à être en mesure de préciser une boucle et qu'il puisse effectuer plus rapidement qu'une fonction récursive car la boucle est absent constamment mise en place de nouveaux pile d'images.

je cherche spécifiquement si la récursion est plus rapide dans les applications où la récursion est la bonne façon de traiter les données, comme dans certaines fonctions de tri, dans les arbres binaires, etc.

243
demandé sur Carson Myers 2010-04-16 10:42:43
la source

12 ответов

Cela dépend de la langue utilisée. Vous avez écrit " langue-agnostique, donc je vais donner quelques exemples.

en Java, C, et Python, la récursion est assez coûteuse par rapport à l'itération (en général) parce qu'elle nécessite l'allocation d'un nouveau cadre de pile. Dans certains compilateurs C, on peut utiliser un drapeau de compilateur pour éliminer ce overhead, qui transforme certains types de récursion (en fait, certains types d'appels de queue) en sauts au lieu d'appels de fonction.

dans les implémentations de langage de programmation fonctionnel, parfois, l'itération peut être très coûteuse et la récursion peut être très bon marché. Dans beaucoup, la récursion est transformée en un simple saut, mais changer la variable de boucle (qui est mutable) parfois nécessite des opérations relativement lourdes, en particulier sur les implémentations qui prennent en charge plusieurs threads d'exécution. La Mutation coûte cher dans certains de ces environnements en raison de l'interaction entre le mutateur et le collecteur d'ordures, si les deux peuvent fonctionner en même temps.

je sais que dans certaines implémentations de Scheme, la récursion sera généralement plus rapide que la boucle.

En bref, la réponse dépend du code et de la mise en œuvre. Utiliser quelque soit le style que vous préférez. Si vous utilisez un langage fonctionnel, la récursion pourrait être plus rapide. Si vous utilisez un langage impératif, l'itération est probablement plus rapide. Dans certains environnements, les deux méthodes aboutissent au même assemblée généré (mettre cela dans votre pipe et la fumée).

Addendum: dans certains environnements, la meilleure alternative n'est ni la récursion ni l'itération mais plutôt des fonctions d'ordre supérieur. Il s'agit de "map", "filter", et "reduce" (qui est également appelé "fold"). Non seulement ces le style préféré, non seulement ils sont souvent plus propre, mais dans certains environnements ces fonctions sont les premières (ou les seules) à obtenir un coup de pouce de la parallélisation automatique - de sorte qu'elles peuvent être beaucoup plus rapides que l'itération ou la récursion. Parallèle des données Haskell est un exemple d'un tel environnement.

liste d'interprétations sont une autre alternative, mais ils sont généralement juste du sucre syntaxique pour l'itération, la récursion, ou des fonctions d'ordre plus élevé.

311
répondu Dietrich Epp 2010-04-16 11:45:09
la source

la récursivité est toujours plus rapide qu'une boucle?

Non, itération sera toujours plus rapide que la récursion. (dans une Architecture de Von Neumann)

explication:

si vous construisez les opérations minimales d'un ordinateur générique à partir de zéro, "itération" vient en premier en tant que bloc de construction et est moins exigeant en ressources que "récursion", ergo est plus rapide.

construire une pseudo-machine à partir de zéro:

posez vous la question : de quoi avez-vous besoin pour calculer une valeur, i.e. pour suivre un algorithme et atteindre un résultat?

nous allons établir une hiérarchie de concepts, en partant de zéro et en définissant en premier lieu les concepts de base, puis construire des concepts de deuxième niveau avec ceux-ci, et ainsi de suite.

  1. Premier Concept: cellules de Mémoire, de stockage, de l'État . Pour faire quelque chose vous avez besoin places pour stocker les valeurs de résultat final et intermédiaire. Supposons que nous ayons un tableau infini de cellules" entières", appelées mémoire , M[0..Infini.]

  2. Instructions: faire quelque chose-transformer une cellule, changer de sa valeur. alter état . Chaque instruction intéressante effectue une transformation. Les instructions de base sont:

    a) d'établir et de déplacer des cellules de mémoire

    • stocker une valeur en mémoire, p.ex.: stocker 5 m[4]
    • copier une valeur à une autre position: p.ex.: stocker m[4] m [8]

    b) logique et arithmétique

    • et, ou, xor, Non
    • add, sub, mul, div. par exemple ajouter m[7] m[8]
  3. Agent d'Exécution : un de base dans un PROCESSEUR récent. Un "agent" est quelque chose qui peut exécutez les instructions. Un Agent peut aussi être une personne qui suit l'algorithme sur papier.

  4. Ordre des étapes: une séquence d'instructions : i.e.: faites ceci en premier, Faites ceci après, etc. Une séquence d'instructions impérative. Même une ligne les expressions sont"une séquence impérative d'instructions". Si vous avez une expression avec un "ordre de l'évaluation", alors vous avez étapes . Cela signifie que même une seule expression composée a des "étapes" implicites et aussi une variable locale implicite (appelons-la "résultat"). par exemple:

    4 + 3 * 2 - 5
    (- (+ (* 3 2) 4 ) 5)
    (sub (add (mul 3 2) 4 ) 5)  
    

    l'expression ci-dessus implique trois étapes avec une variable" résultat " implicite.

    // pseudocode
    
           1. result = (mul 3 2)
           2. result = (add 4 result)
           3. result = (sub result 5)
    

    donc même les expressions infix, puisque vous avez un ordre spécifique d'évaluation, sont une séquence impérative d'instructions . L'expression implique une séquence d'opérations à effectuer dans un ordre déterminé, et parce qu'il y a des étapes , il y a aussi une variable intermédiaire" Résultat " implicite.

  5. indicateur D'Instruction : si vous avez une séquence d'étapes, vous avez aussi un "indicateur d'instruction" implicite. Le pointeur d'instruction marque la prochaine instruction, et avance après le instruction est lu mais avant que l'instruction soit exécutée.

    dans cette pseudo-machine de calcul, le pointeur D'Instruction fait partie de mémoire . (Note: Normalement le pointeur D'Instruction sera un "registre spécial" dans un noyau CPU, mais ici nous allons simplifier les concepts et supposer que toutes les données (registres inclus) font partie de" mémoire")

  6. Sauter - Une fois que vous avez un nombre commandé d'étapes et un indicateur D'Instruction , vous pouvez appliquer le " magasin " instruction pour modifier la valeur du pointeur D'Instruction elle-même. Nous appellerons cette utilisation spécifique de la store "d'instruction 1519120920" avec un nouveau nom: Sauter . Nous utilisons un nouveau nom parce qu'il est plus facile de le considérer comme un nouveau concept. En modifiant le pointeur d'instruction, nous ordonnons à l'agent d'aller à l'étape x".

  7. Infini Itération : Par sauter en arrière, maintenant, vous pouvez faire de l'agent "répéter" un certain nombre d'étapes. À ce point nous avons itération infinie.

                       1. mov 1000 m[30]
                       2. sub m[30] 1
                       3. jmp-to 2  // infinite loop
    
  8. Conditionnel - à condition que l'exécution des instructions. Avec la clause "conditionnelle" , vous pouvez exécuter conditionnellement une ou plusieurs instructions basées sur l'état actuel (qui peut être défini avec une instruction précédente).

  9. bon itération : maintenant avec la conditionnelle clause, nous pouvons échapper à la boucle infinie de la saut en arrière instruction. Nous avons maintenant une boucle conditionnelle et puis bonne itération

    1. mov 1000 m[30]
    2. sub m[30] 1
    3. (if not-zero) jump 2  // jump only if the previous 
                            // sub instruction did not result in 0
    
    // this loop will be repeated 1000 times
    // here we have proper ***iteration***, a conditional loop.
    
  10. Naming : donner des noms à un emplacement de mémoire contenant des données ou de la tenue d'une étape . C'est juste une "commodité" pour avoir. Nous n'ajoutons pas de nouvelles instructions en ayant la capacité de définir des "noms" pour les emplacements de mémoire. "Naming" n'est pas une instruction de l'agent, c'est juste un confort pour nous. nommer rend le code (à ce point) plus facile à lire et plus facile à modifier.

       #define counter m[30]   // name a memory location
       mov 1000 counter
    loop:                      // name a instruction pointer location
        sub counter 1
        (if not-zero) jmp-to loop  
    
  11. sous-programme à un niveau : supposons qu'il y ait une série d'étapes que vous devez exécuter fréquemment. Vous pouvez stocker les étapes dans une position nommée en mémoire et ensuite passer à cette position lorsque vous avez besoin de les exécuter (appel). À la fin de la séquence, vous aurez besoin de retour au point de appel pour continuer exécution. Avec ce mécanisme, vous êtes créant de nouvelles instructions (sous-programmes) en composant des instructions de base.

    mise en Œuvre: (pas de nouveaux concepts requis)

    • stocker le pointeur D'Instruction actuel dans une position de mémoire prédéfinie
    • sauter à la sous-routine
    • à la fin du sous-programme, vous récupérer le pointeur D'Instruction à partir de l'emplacement de mémoire prédéfini, en sautant effectivement à l'instruction suivante de l'original appel

    problème avec l'implémentation à un niveau : vous ne pouvez pas appeler un autre sous-programme à partir d'un sous-programme. Si vous le faites, vous écraserez l'adresse de retour (variable globale), de sorte que vous ne pouvez pas imiter les appels.

    pour avoir un meilleur Mise en œuvre pour les sous-programmes: vous avez besoin d'une pile

  12. Stack : vous définissez un espace mémoire pour travailler comme une "pile", vous pouvez "pousser" les valeurs sur la pile, et aussi "pop" la dernière valeur "poussée". Pour implémenter une pile, vous aurez besoin d'un pointeur de pile (similaire au pointeur D'Instruction) qui pointe vers la "tête" réelle de la pile. Lorsque vous "poussez" une valeur, le empilez les décréments de pointeur et vous stockez la valeur. Lorsque vous "pop", vous obtenez la valeur au Pointeur de Pile, puis le Pointeur de Pile est incrémenté.

  13. sous-Programmes maintenant que nous avons un stack nous pouvons mettre en œuvre les sous-programmes appropriés permettant les appels imbriqués . L'implémentation est similaire, mais au lieu de stocker le pointeur D'Instruction dans une position mémoire prédéfinie, nous " poussons "la valeur de L'IP dans la pile . À la fin du sous-programme, nous "pop" juste la valeur de la pile, en sautant effectivement de nouveau à l'instruction après l'original appel . Cette implémentation, ayant une" pile " permet d'appeler un sous-programme à partir d'un autre sous-programme. Avec cette implémentation nous pouvons créer plusieurs niveaux d'abstraction en définissant nouvelles instructions comme sous-programmes, en utilisant des instructions de base ou d'autres sous-programmes comme éléments constitutifs.

  14. récursion : que se passe-t-il lorsqu'un sous-programme s'appelle lui-même?. Ceci est appelé la "récursivité".

    Problème: l'Écrasement de la locale les résultats intermédiaires d'un sous-programme peut être stocker dans la mémoire. Puisque vous appelez/réutilisez les mêmes étapes, si les résultats intermédiaires sont stockés dans des emplacements de mémoire prédéfinis (variables globales) ils seront écrasés sur les appels imbriqués.

    Solution: pour permettre la récursion, les sous-programmes doivent stocker les résultats intermédiaires locaux dans la pile , donc, sur chaque appel récursif (direct ou indirect) les résultats intermédiaires sont stockés dans des emplacements de mémoire différents.

...

ayant atteint récursion nous nous arrêtons ici.

Conclusion:

dans une Architecture Von Neumann, clairement " itération " est un concept plus simple/de base que " récursion " ". Nous avons un formulaire de "Itération " au niveau 7, tandis que " récursion " se situe au niveau 14 de la hiérarchie des concepts.

itération sera toujours plus rapide dans le code machine parce qu'il implique moins d'instructions donc moins de cycles CPU.

lequel est "meilleur"?

  • vous devez utiliser "itération" lorsque vous traitez simple, séquentielle des structures de données, et partout une" boucle simple " fera l'affaire.

  • vous devez utiliser "recursion" lorsque vous avez besoin de traiter une structure de données récursive (j'aime les appeler "Structures de données fractales"), ou lorsque la solution récursive est clairement plus "élégante".

Conseils : utiliser le meilleur outil pour le travail, mais de comprendre le fonctionnement interne de chaque outil afin de choisir judicieusement.

enfin, notez que vous avez beaucoup d'occasions d'utiliser la récursion. Vous avez structures de données récursives partout, vous en regardez une maintenant: des parties du DOM supportant ce que vous lisez sont un RDS, une expression JSON est un RDS, le système de fichiers hiérarchique dans votre ordinateur est un RDS, I. e: vous avez un répertoire racine, contenant des fichiers et des répertoires, chaque répertoire contenant des fichiers et des répertoires, chacun de ces répertoires contenant des fichiers et des répertoires...

41
répondu Lucio M. Tato 2017-08-27 05:03:28
la source

la récursion peut être plus rapide lorsque l'alternative est de gérer explicitement une pile, comme dans les algorithmes de tri ou d'arborescence binaire que vous mentionnez.

j'ai eu un cas où réécrire un algorithme récursif en Java l'a rendu plus lent.

donc la bonne approche est d'abord de l'écrire de la manière la plus naturelle, optimiser seulement si le profilage montre qu'il est critique, et ensuite mesurer l'amélioration supposée.

32
répondu starblue 2010-04-16 22:13:47
la source

Recursion de la queue est aussi rapide que la boucle. De nombreux langages fonctionnels ont la récursion de la queue mise en œuvre dans eux.

12
répondu mkorpela 2017-05-23 15:26:20
la source

réfléchissez à ce qui doit absolument être fait pour chaque itération et récursion.

  • itération: un saut au début de boucle
  • récursivité: un saut au début de la fonction appelée

Vous voyez qu'il n'y a pas beaucoup de place pour les différences ici.

(je suppose que la récursion est un appel de queue et que le compilateur est conscient de cette optimisation).

12
répondu Pasi Savolainen 2010-04-16 11:38:43
la source

la plupart des réponses ici oublient le coupable évident pourquoi la récursion est souvent plus lente que les solutions itératives. Il est lié à l'accumulation et la destruction des cadres de pile, mais ce n'est pas exactement cela. C'est généralement une grande différence dans le stockage de l'auto variable pour chaque récursion. Dans un algorithme itératif avec boucle, les variables sont souvent conservées dans des registres et même si elles se déversent, elles résideront dans le cache de niveau 1. Dans un algorithme récursif, tous les états intermédiaires de la variable sont stockées sur la pile, ce qui signifie qu'ils engendrent beaucoup plus de déversements de mémoire. Cela signifie que même si elle fait la même quantité d'opérations, elle aura beaucoup d'accès mémoire dans la boucle chaude et ce qui rend le pire, ces opérations mémoire ont un taux de réutilisation moche rendant les caches moins efficace.

TL; DR les algorithmes récursifs ont généralement un comportement de cache pire que les algorithmes itératifs.

8
répondu Patrick Schlüter 2016-09-05 15:45:17
la source

la plupart des réponses ici sont wrong . La bonne réponse est cela dépend . Par exemple, voici deux fonctions C qui passe à travers un arbre. D'abord le récursif:

static
void mm_scan_black(mm_rc *m, ptr p) {
    SET_COL(p, COL_BLACK);
    P_FOR_EACH_CHILD(p, {
        INC_RC(p_child);
        if (GET_COL(p_child) != COL_BLACK) {
            mm_scan_black(m, p_child);
        }
    });
}

et voici la même fonction implémentée en utilisant l'itération:

static
void mm_scan_black(mm_rc *m, ptr p) {
    stack *st = m->black_stack;
    SET_COL(p, COL_BLACK);
    st_push(st, p);
    while (st->used != 0) {
        p = st_pop(st);
        P_FOR_EACH_CHILD(p, {
            INC_RC(p_child);
            if (GET_COL(p_child) != COL_BLACK) {
                SET_COL(p_child, COL_BLACK);
                st_push(st, p_child);
            }
        });
    }
}

il n'est pas important de comprendre les détails du code. Juste que p sont des noeuds et que P_FOR_EACH_CHILD ne marche. Dans la version itérative, nous avons besoin d'une pile explicite st sur laquelle les noeuds sont poussés puis déclenchés et manipulés.

La fonction récursive tourne beaucoup plus vite que l'itératif. La raison en est que dans ce dernier, pour chaque item, un CALL à la fonction st_push est nécessaire et puis un autre à st_pop .

dans le premier, vous avez seulement le récursif CALL pour chaque noeud.

de plus, l'accès aux variables sur la callstack est incroyablement rapide. Cela signifie que vous lisez de mémoire qui est susceptible d'être toujours dans la cache la plus intérieure. Une pile explicite, d'autre part, doit être soutenue par malloc :mémoire ed du tas qui est beaucoup plus lente à accéder.

avec une optimisation soigneuse, comme st_push et st_pop , je peux atteindre à peu près la parité avec l'approche récursive. Mais au moins, sur mon ordinateur, le coût de accéder à la mémoire du tas est plus grand que le coût de l'appel récursif.

mais cette discussion est surtout discutable parce que la marche récursive de l'arbre est incorrecte . Si vous avez un arbre assez grand, vous manquerez d'espace dans la callstack, c'est pourquoi un algorithme itératif doit être utilisé.

6
répondu Björn Lindqvist 2016-04-18 05:00:09
la source

dans n'importe quel système réaliste, non, la création d'un cadre stack sera toujours plus chère qu'un INC et un JMP. C'est pourquoi les très bons compilateurs transforment automatiquement la récursion de la queue en un appel vers le même cadre, c'est-à-dire sans la tête, de sorte que vous obtenez la version source plus lisible et la version compilée plus efficace. Un vraiment, vraiment bon compilateur devrait même être capable de transformer la récursion normale en récursion de queue où cela est possible.

2
répondu Kilian Foth 2010-04-16 10:54:45
la source

programmation Fonctionnelle est plus à propos de " ce "plutôt que de" comment ".

La langue des réalisateurs vont trouver un moyen d'optimiser le fonctionnement du code en dessous, si nous n'essayons pas de le rendre plus optimisé qu'il doit être. La récursion peut également être optimisée dans les langages qui prennent en charge l'optimisation des appels de queue.

ce qui importe le plus du point de vue du programmeur, c'est la lisibilité et la maintenabilité plutôt que l'optimisation en premier lieu. Encore une fois, "l'optimisation prématurée est la racine de tout mal".

1
répondu noego 2016-12-01 06:53:01
la source

c'est une supposition. En général, la récursion ne bat probablement pas souvent ou jamais les boucles sur les problèmes de taille décente si les deux utilisent de très bons algorithmes (sans compter la difficulté d'implémentation) , il peut être différent si utilisé avec un langage w/ retail call recursion (et un algorithme de récursion de la queue et avec des boucles aussi comme partie de la langue)-qui aurait probablement très similaire et peut-être même préfère la récursion une partie du temps.

0
répondu Roman A. Taycher 2010-04-16 11:04:59
la source

en général, non, la récursion ne sera pas plus rapide qu'une boucle dans n'importe quel usage réaliste qui a des implémentations viables dans les deux formes. Je veux dire, bien sûr, vous pourriez coder des boucles qui prennent une éternité, mais il y aurait de meilleures façons d'implémenter la même boucle qui pourrait surpasser n'importe quelle implémentation du même problème via la récursion.

vous avez frappé le clou sur la tête en ce qui concerne la raison; la création et la destruction des cadres de pile est plus chère qu'un simple saut.

Toutefois, notez que j'ai dit "a viable implémentations dans les deux formes". Pour des choses comme beaucoup d'algorithmes de tri, il tend à ne pas y avoir une façon très viable de les mettre en œuvre qui ne met pas efficacement en place sa propre version d'une pile, en raison de la reproduction de l'enfant "tâches" qui font intrinsèquement partie du processus. Ainsi, la récursion peut être tout aussi rapide que la tentative d'implémenter l'algorithme via une boucle.

éditer: cette réponse suppose non-fonctionnelle langues, où la plupart des types de données de base sont mutables. Elle ne s'applique pas aux langages fonctionnels.

0
répondu Amber 2010-04-16 11:22:58
la source

selon la théorie, c'est les mêmes choses. La récursion et la boucle avec la même complexité O() fonctionneront avec la même vitesse théorique, mais bien sûr la vitesse réelle dépend du langage, du compilateur et du processeur. Exemple avec la puissance du nombre peut être codé de manière itérative avec O(ln (n)):

  int power(int t, int k) {
  int res = 1;
  while (k) {
    if (k & 1) res *= t;
    t *= t;
    k >>= 1;
  }
  return res;
  }
0
répondu Hydrophis Spiralis 2010-04-16 12:01:32
la source

Autres questions sur performance recursion loops iteration