web-dev-qa-db-fra.com

Récursivité ou boucles while

Je lisais sur certaines pratiques d'entrevue de développement, en particulier sur les questions techniques et les tests demandés lors des entrevues et j'ai trébuché à plusieurs reprises sur les dictons du genre "Ok, vous avez résolu le problème avec une boucle while, maintenant pouvez-vous le faire avec récursivité ", ou" tout le monde peut résoudre ce problème avec une boucle while de 100 lignes, mais peuvent-ils le faire dans une fonction récursive de 5 lignes? " etc.

Ma question est la suivante: la récursivité est-elle généralement meilleure que si/while/pour les constructions?

Honnêtement, j'ai toujours pensé que la récursivité n'est pas à privilégier, car elle est limitée à la mémoire de la pile qui est beaucoup plus petite que le tas, également faire un grand nombre d'appels de fonction/méthode est sous-optimal du point de vue des performances, mais je peux se tromper...

128
Shivan Dragon

La récursivité n'est pas intrinsèquement meilleure ou pire que les boucles - chacune a ses avantages et ses inconvénients, et ceux-ci dépendent même du langage de programmation (et de la mise en œuvre).

Techniquement, les boucles itératives s'adaptent mieux aux systèmes informatiques typiques au niveau matériel: au niveau du code machine, une boucle n'est qu'un test et un saut conditionnel, tandis que la récursivité (implémentée naïvement) implique de pousser un cadre de pile, de sauter, de retourner et de revenir en arrière de la pile. OTOH, de nombreux cas de récursivité (en particulier ceux qui sont trivialement équivalents à des boucles itératives) peuvent être écrits de sorte que la pile Push/pop puisse être évitée; cela est possible lorsque l'appel de fonction récursive est la dernière chose qui se produit dans le corps de la fonction avant de revenir, et il est communément appelé optimisation de l'appel de queue (ou optimisation de la récursivité de la queue) . Une fonction récursive correctement optimisée pour les appels de queue est généralement équivalente à une boucle itérative au niveau du code machine.

Une autre considération est que les boucles itératives nécessitent des mises à jour destructives de l'état, ce qui les rend incompatibles avec la sémantique du langage pur (sans effets secondaires). C'est la raison pour laquelle les langages purs comme Haskell n'ont pas du tout de constructions en boucle, et de nombreux autres langages de programmation fonctionnelle en manquent complètement ou les évitent autant que possible.

Cependant, la raison pour laquelle ces questions apparaissent autant dans les entretiens est que, pour y répondre, vous avez besoin d'une compréhension approfondie de nombreux concepts de programmation vitaux - variables, appels de fonction, portée, et bien sûr boucles et récursivité -, et vous avez pour apporter la flexibilité mentale à la table qui vous permet d'aborder un problème sous deux angles radicalement différents, et de vous déplacer entre différentes manifestations du même concept.

L'expérience et la recherche suggèrent qu'il existe une frontière entre les personnes qui ont la capacité de comprendre les variables, les pointeurs et la récursivité, et celles qui ne le font pas. Presque tout le reste dans la programmation, y compris les frameworks, les API, les langages de programmation et leurs cas Edge, peut être acquis par l'étude et l'expérience, mais si vous ne parvenez pas à développer une intuition pour ces trois concepts de base, vous n'êtes pas apte à être programmeur. La traduction d'une simple boucle itérative en une version récursive est le moyen le plus rapide de filtrer les non-programmeurs - même un programmeur plutôt inexpérimenté peut généralement le faire en 15 minutes, et c'est un problème très indépendant du langage, donc le candidat peut choisir une langue de leur choix au lieu de trébucher sur des idiosyncrasies.

Si vous obtenez une question comme celle-ci dans une interview, c'est un bon signe: cela signifie que l'employeur potentiel recherche des personnes qui peuvent programmer, pas des personnes qui ont mémorisé le manuel d'un outil de programmation.

197
tdammers

Ça dépend.

  • Certains problèmes se prêtent très bien à des solutions récursives, par exemple tri rapide
  • Certaines langues ne prennent pas vraiment en charge la récursivité, par exemple premiers FORTRAN
  • Certaines langues supposent que la récursivité est un moyen principal de bouclage, par exemple Haskell

Il convient également de noter que la prise en charge de récursion de la queue rend les boucles récursives et itératives équivalentes, c'est-à-dire que la récursivité n'a pas toujours à gaspiller la pile.

De plus, un algorithme récursif peut toujours être implémenté de manière itérative en utilisant une pile explicite .

Enfin, je noterais qu'une solution à cinq lignes est probablement toujours meilleure qu'une solution à 100 lignes (en supposant qu'elles soient réellement équivalentes).

37
jk.

Il n'y a pas de définition universellement acceptée de "meilleur" en matière de programmation, mais je vais le considérer comme signifiant "plus facile à maintenir/à lire".

La récursivité a plus de puissance expressive que les constructions de boucles itératives: je dis cela parce qu'une boucle while est équivalente à une fonction récursive de queue et les fonctions récursives n'ont pas besoin d'être récursives de queue. Les constructions puissantes sont généralement une mauvaise chose car elles vous permettent de faire des choses difficiles à lire. Cependant, la récursivité vous donne la possibilité d'écrire des boucles sans utiliser de mutabilité et, à mon avis, la mutabilité est beaucoup plus puissante que la récursivité.

Ainsi, d'une puissance expressive faible à une puissance expressive élevée, les constructions en boucle s'empilent comme suit:

  • Fonctions récursives de queue qui utilisent des données immuables,
  • Fonctions récursives qui utilisent des données immuables,
  • Alors que les boucles qui utilisent des données mutables,
  • Fonctions récursives de queue qui utilisent des données mutables,
  • Fonctions récursives qui utilisent des données mutables,

Idéalement, vous utiliseriez les constructions les moins expressives possibles. Bien sûr, si votre langue ne prend pas en charge l'optimisation des appels de queue, cela peut également influencer votre choix de construction en boucle.

17
dan_waterworth

La récursivité est souvent moins évidente. Moins évident est plus difficile à maintenir.

Si vous écrivez for(i=0;i<ITER_LIMIT;i++){somefunction(i);} dans le flux principal, vous précisez parfaitement que vous écrivez une boucle. Si vous écrivez somefunction(ITER_LIMIT);, vous ne précisez pas vraiment ce qui va se passer. Voir uniquement le contenu: cette somefunction(int x) appelle somefunction(x-1) vous indique qu'il s'agit en fait d'une boucle utilisant des itérations. De plus, vous ne pouvez pas facilement mettre une condition d'échappement avec break; Quelque part à mi-chemin des itérations, vous devez soit ajouter une condition qui sera passée tout le chemin du retour, soit lever une exception. (et les exceptions ajoutent encore de la complexité ...)

En substance, si c'est un choix évident entre l'itération et la récursivité, faites la chose intuitive. Si l'itération fait le travail facilement, la sauvegarde de 2 lignes vaut rarement les maux de tête qu'elle peut créer à long terme.

Bien sûr, si cela vous fait économiser 98 lignes, c'est une tout autre affaire.

Il y a des situations pour lesquelles la récursivité convient tout simplement parfaitement, et elles ne sont pas vraiment rares. Traversée de structures arborescentes, de réseaux à liaisons multiples, de structures pouvant contenir son propre type, de tableaux multidimensionnels dentelés, essentiellement tout ce qui n'est ni un vecteur simple ni un tableau de dimensions fixes. Si vous traversez un chemin droit connu, itérez. Si vous plongez dans un lieu inconnu, demandez de l'aide.

Essentiellement, si somefunction(x-1) doit être appelé depuis lui-même plus d'une fois par niveau, oubliez les itérations.

... L'écriture itérative de fonctions pour des tâches qui sont mieux effectuées par récursivité est possible mais pas agréable. Partout où vous utiliseriez int, vous avez besoin de quelque chose comme stack<int>. Je l'ai fait une fois, plus comme exercice que pour des raisons pratiques. Je peux vous assurer qu'une fois que vous serez confronté à une telle tâche, vous n'aurez pas de doutes comme ceux que vous avez exprimés.

8
SF.

Comme d'habitude, cela est sans réponse en général car il existe des facteurs supplémentaires, qui dans la pratique sont largement inégaux entre les cas et inégaux entre eux dans un cas d'utilisation. Voici quelques-unes des pressions.

  • Le code court et élégant est en général supérieur au code long et complexe.
  • Cependant, le dernier point est quelque peu invalidé si votre base de développeurs n'est pas familière avec la récursivité et ne veut pas/ne peut pas apprendre. Il peut même devenir un léger négatif plutôt qu'un positif.
  • La récursivité peut être mauvaise pour l'efficacité si vous aurez besoin d'appels profondément imbriqués dans la pratique et vous ne pouvez pas utiliser la récursivité de queue (ou votre environnement ne peut pas optimiser la récursivité de queue).
  • La récursivité est également mauvaise dans de nombreux cas si vous ne pouvez pas correctement mettre en cache les résultats intermédiaires. Par exemple, l'exemple courant d'utilisation de la récursion d'arbre pour calculer les nombres de Fibonacci donne horriblement mauvais si vous ne mettez pas en cache. Si vous cachez, c'est simple, rapide, élégant et tout à fait merveilleux.
  • La récursivité n'est pas applicable à certains cas, aussi bonne que l'itération dans d'autres, et absolument nécessaire dans d'autres encore. Passer à travers de longues chaînes de règles métier n'est généralement pas du tout aidé par la récursivité. L'itération dans les flux de données peut être utilement effectuée avec la récursivité. Itérer sur des structures de données dynamiques multidimensionnelles (par exemple des labyrinthes, des arbres d'objets ...) est à peu près impossible sans récursivité, explicite ou implicite. Notez que dans ces cas, la récursivité explicite est beaucoup meilleure qu'implicite - rien n'est plus douloureux que de lire du code où quelqu'un a implémenté sa propre pile de bogues ad-hoc, incomplète dans le langage juste pour éviter le R effrayant -Mot.
6
Kilian Foth

Cela dépend vraiment de la commodité ou de l'exigence:

Si vous prenez le langage de programmation Python , il prend en charge la récursivité, mais par défaut il y a une limite pour la profondeur de récursivité (1000). S'il dépasse la limite, nous obtiendrons une erreur ou une exception. Cette limite peut être modifiée, mais si nous le faisons, nous pourrions rencontrer des situations anormales dans la langue.

À ce stade (nombre d'appels de plus que la profondeur de récursivité), nous devons préférer les constructions de boucles. Je veux dire, si la taille de la pile n'est pas suffisante, nous devons préférer les constructions de boucle.

1
neotam

La récursion consiste à répéter l'appel à la fonction, la boucle à répéter le saut à placer en mémoire.

Devrait également être mentionné à propos du débordement de pile - http://en.wikipedia.org/wiki/Stack_overflow

1
okobaka