web-dev-qa-db-fra.com

Y a-t-il des avantages à utiliser la récursivité par rapport à l'itération - autres que parfois la lisibilité et l'élégance?

Je suis sur le point de faire deux hypothèses. Veuillez me corriger s'ils se trompent:

  1. Il n'y a pas d'algorithme récursif sans équivalent itératif.
  2. L'itération est toujours moins chère en termes de performances que la récursivité (au moins dans les langages à usage général tels que Java, C++, Python etc.).

S'il est vrai que la récursivité est toujours plus coûteuse que l'itération, et qu'elle peut toujours être remplacée par un algorithme itératif (dans les langues qui le permettent) - alors je pense que les deux autres raisons d'utiliser la récursivité sont: l'élégance et la lisibilité.

Certains algorithmes sont exprimés plus élégamment avec la récursivité. Par exemple. numérisation d'un arbre binaire.

Cependant, à part cela, y a-t-il des raisons d'utiliser la récursivité sur l'itération? La récursivité a-t-elle des avantages sur l'itération autres que parfois l'élégance et la lisibilité?

8
Aviv Cohn

Eh bien, c'est généralement moins de code.

Et dans la mesure où c'est moins de code, c'est moins sujet aux erreurs.

En particulier, la récursivité est très bénéfique lorsque les solutions itératives nécessitent que vous simulez la récursivité avec une pile. La récursivité reconnaît que le compilateur gère déjà une pile pour accomplir précisément ce dont vous avez besoin. Lorsque vous commencez à gérer le vôtre, non seulement vous réintroduisez probablement la surcharge d'appel de fonction que vous aviez l'intention d'éviter; mais vous réinventez une roue (avec beaucoup de place pour les bugs) qui existe déjà sous une forme assez exempte de bugs.

OMI, n'évitez la récursivité que si cela peut se faire naturellement et facilement sans pile. (Ou autre structure de gestion d'états et/ou d'étendues non scalaires.)

12
svidgen

Premièrement, s'il est vrai qu'il existe toujours un équivalent itératif équivalent à tout algorithme récursif, il n'est pas nécessairement le cas que l'équivalent itératif soit meilleur, pour toute définition raisonnable de "mieux".

Pour certains algorithmes, l'équivalent itératif se termine simplement en simulant l'algorithme récursif, avec un paramètre simulé et un empilement et un dépilage de variables locales. Considérez Fonction d'Ackermann . Considérez codage Huffman . Dans ces cas, il y a très peu d'avantages à (ré) écrire les opérations de pile explicites.

Deuxièmement, il n'est pas nécessairement vrai que la récursivité coûte toujours plus cher que l'itération. Considérez récursivité de la queue , et lisez les articles "Lambda: The Ultimate ...".

(Oui, je sais que cela doit être élargi. Je dois aller à un rendez-vous chez le médecin dès maintenant.)

OK, il y a des choses à dire.

Tony Hoare a initialement décrit Quicksort de manière itérative, et il a déclaré que c'était très difficile à expliquer. Expliquée récursivement, elle est SIMPLE. Voir Quicksort dans Haskell pour les détails. Si le tableau a une longueur égale à un, vous avez terminé, sinon, vous devez le trier. Tout d'abord, vous choisissez un élément pivot. Traditionnellement, c'est le premier élément, mais n'importe quel élément peut être utilisé. Ensuite, vous effectuez un passage sur le tableau, pour former trois sous-réseaux. Le premier est tous les éléments inférieurs au pivot, le second est tous les éléments égaux au pivot et le troisième est tous les éléments supérieurs au pivot. (Si les valeurs des éléments du tableau doivent être uniques, la longueur du deuxième sous-tableau est égale à un.) Maintenant, tri rapide du premier sous-tableau, puis tri rapide du deuxième sous-tableau.

La recherche binaire est plus facile à comprendre lorsqu'elle est présentée récursivement, et elle est en fait récursive. Vous sondez l'élément central du tableau. S'il est égal à la valeur que vous recherchez, vous avez terminé. Si l'élément du milieu est supérieur à la valeur que vous recherchez, vous savez que la valeur souhaitée doit se situer "à gauche" et vous recherchez le sous-tableau avant l'élément du milieu, sinon vous recherchez le sous-tableau après l'élément du milieu. Dans les deux cas, vous savez que l'élément du milieu n'est pas la cible, alors laissez-le de côté. Soit vous trouvez votre cible, soit vous manquez de tableau pour effectuer une recherche. À ce moment-là, vous vous libérez complètement et vous avez terminé. En 2014, sans parler de 2019, tout compilateur qui se respecte sait comment faire une optimisation de récursivité de queue, même Java compilateurs. (Remarque: Le classique Java la machine virtuelle ne prend pas en charge l'optimisation générale des appels de queue, faute d'une opération GOTO générale, mais cela n'affecte pas l'optimisation de la récursivité de queue.)

Le vrai problème, à de nombreux endroits, est que les personnes qui dirigent l'émission n'ont jamais appris l'optimisation de la récursivité de la queue, et donc elles interdisent toute récursivité. Je suis tombé sur cela chez Nortel Networks, et j'ai dû écrire une page complète de commentaires l'expliquant et certains concepts connexes [~ # ~] et [~ # ~] leur montrer les listes de langage d'assemblage qui ont prouvé que le compilateur ne générait PAS réellement d'appels récursifs. Nortel a disparu maintenant, mais ces gestionnaires existent toujours dans de nombreux endroits.

J'espère que cela t'aides.

4
John R. Strohm

La récursivité utilise la pile d'appels pour stocker les retours d'appels de fonction. L'état de la fonction est stocké entre les appels.

L'itération doit également utiliser une pile ou un mécanisme similaire pour stocker les états intermédiaires, sauf que vous créez la pile vous-même. À moins, bien sûr, que vous ne trouviez un algorithme de remplacement qui ne nécessite pas un tel stockage d'état.

La récursivité n'est plus coûteuse que si vous débordez la pile. Dans un sens, l'itération va être plus coûteuse (dans les algorithmes qui se prêtent à la récursivité), car vous recréez le mécanisme de stockage d'état que la récursion fournit déjà.

3
Robert Harvey