Je me suis demandé si une boucle while est intrinsèquement une récursivité?
Je pense que c'est parce qu'une boucle while peut être considérée comme une fonction qui s'appelle à la fin. S'il ne s'agit pas d'une récursivité, alors quelle est la différence?
Les boucles sont très bien pas récursivité. En fait, ils sont le meilleur exemple du mécanisme opposé: itération.
Le point de récursivité est qu'un élément du traitement appelle une autre instance de lui-même. La machine de contrôle de boucle simplement saute au point où elle a commencé.
Sauter dans le code et appeler un autre bloc de code sont des opérations différentes. Par exemple, lorsque vous passez au début de la boucle, la variable de contrôle de boucle a toujours la même valeur qu'elle avait avant le saut. Mais si vous appelez une autre instance de la routine dans laquelle vous vous trouvez, alors la nouvelle instance a nouvelle, sans rapport copies de toutes ses variables. En effet, une variable peut avoir une valeur au premier niveau de traitement et une autre valeur à un niveau inférieur.
Cette fonctionnalité est cruciale pour que de nombreux algorithmes récursifs fonctionnent, et c'est pourquoi vous ne pouvez pas émuler la récursivité via l'itération sans gérer également une pile de trames appelées qui garde une trace de toutes ces valeurs.
Cela dépend de votre point de vue.
Si vous regardez théorie de la calculabilité, l'itération et la récursivité sont également expressives . Cela signifie que vous pouvez écrire une fonction qui calcule quelque chose, et peu importe que vous le fassiez récursivement ou itérativement, vous pourrez choisir les deux approches. Il n'y a rien que vous puissiez calculer récursivement que vous ne puissiez pas calculer itérativement et vice versa (bien que le fonctionnement interne du programme puisse être différent).
De nombreux langages de programmation ne traitent pas la récursivité et l'itération de la même manière, et pour cause. Habituellement , la récursivité signifie que le langage/compilateur gère la pile des appels, et l'itération signifie que vous devrez peut-être gérer vous-même la pile.
Cependant, il existe des langages - en particulier des langages fonctionnels - dans lesquels se trouvent des choses comme les boucles (for, while) en effet uniquement du sucre syntaxique pour la récursivité et implémenté en coulisses de cette façon. Cela est souvent souhaitable dans les langages fonctionnels, car ils n'ont généralement pas le concept de boucler autrement, et l'ajouter rendrait leur calcul plus complexe, pour peu de raisons pratiques.
Donc non, ils ne sont pas intrinsèquement identiques . Ils sont également expressifs , ce qui signifie que vous ne pouvez pas calculer quelque chose de manière itérative, vous ne pouvez pas calculer récursivement et vice versa, mais c'est à peu près tout, dans le cas général ( selon la thèse de Church-Turing).
Notez que nous parlons ici de programmes récursifs . Il existe d'autres formes de récursivité, par ex. dans les structures de données (par exemple, les arbres).
Si vous le regardez d'un point de vue de l'implémentation, alors la récursivité et l'itération ne sont pas du tout les mêmes. La récursivité crée un nouveau cadre de pile pour chaque appel. Chaque étape de la récursivité est autonome, obtenant les arguments pour le calcul de l'appelé (lui-même).
En revanche, les boucles ne créent pas de trames d'appel. Pour eux, le contexte n'est pas conservé à chaque étape. Pour la boucle, le programme revient simplement au début de la boucle jusqu'à ce que la condition de boucle échoue.
C'est assez important à savoir, car cela peut faire des différences assez radicales dans le monde réel. Pour la récursivité, le contexte entier doit être enregistré à chaque appel. Pour l'itération, vous avez un contrôle précis sur les variables qui sont en mémoire et ce qui est enregistré où.
Si vous le regardez de cette façon, vous voyez rapidement que pour la plupart des langues, l'itération et la récursivité sont fondamentalement différentes et ont des propriétés différentes. Selon la situation, certaines propriétés sont plus souhaitables que d'autres.
La récursivité peut rendre les programmes plus simples et plus faciles à tester et à l'épreuve . La conversion d'une récursivité en itération rend généralement le code plus complexe, augmentant la probabilité d'échec. D'un autre côté, la conversion en itération et la réduction de la quantité de trames de pile d'appels peuvent économiser la mémoire nécessaire.
Dire que X est intrinsèquement Y n'a de sens que si vous avez en tête un système (formel) dans lequel vous exprimez X. Si vous définissez la sémantique de while
en termes de calcul lambda, vous pourriez mentionner récursivité *; si vous le définissez en termes de machine d'enregistrement, vous ne le ferez probablement pas.
Dans les deux cas, les gens ne vous comprendront probablement pas si vous appelez une fonction récursive simplement parce qu'elle contient une boucle while.
* Bien que peut-être seulement indirectement, par exemple si vous le définissez en termes de fold
.
La différence est la pile implicite et la sémantique.
Une boucle while qui "s'appelle à la fin" n'a pas de pile à analyser quand elle est terminée. C'est la dernière itération qui définit quel sera l'état à la fin.
Cependant, la récursivité ne peut pas être effectuée sans cette pile implicite qui se souvient de l'état du travail effectué auparavant.
Il est vrai que vous pouvez résoudre tout problème de récursivité avec l'itération si vous lui donnez explicitement accès à une pile. Mais ce n'est pas pareil.
La différence sémantique a à voir avec le fait que regarder du code récursif véhicule une idée d'une manière complètement différente du code itératif. Le code itératif fait les choses étape par étape. Il accepte tous les états antérieurs et ne fonctionne que pour créer l'état suivant.
Le code récursif décompose un problème en fractales. Cette petite partie ressemble à cette grande partie afin que nous puissions faire juste cette partie et cette partie de la même manière. C'est une façon différente de penser aux problèmes. C'est très puissant et il faut s'y habituer. Beaucoup peut être dit en quelques lignes. Vous ne pouvez tout simplement pas tirer cela d'une boucle while même si elle a accès à une pile.
Tout dépend de votre utilisation du terme intrinsèquement . Au niveau du langage de programmation, ils sont syntaxiquement et sémantiquement différents, et ils ont des performances et une utilisation de la mémoire assez différentes. Mais si vous creusez assez profondément dans la théorie, ils peuvent être définis les uns par rapport aux autres, et sont donc "les mêmes" dans un certain sens théorique.
La vraie question est: quand est-il judicieux de faire la distinction entre l'itération (boucles) et la récursivité, et quand est-il utile de la considérer comme les mêmes choses? La réponse est que lors de la programmation (par opposition à l'écriture de preuves mathématiques), il est important de distinguer l'itération de la récursivité.
La récursivité crée une nouvelle trame de pile, c'est-à-dire un nouvel ensemble de variables locales pour chaque appel. Cela a une surcharge et occupe de l'espace sur la pile, ce qui signifie qu'une récursion suffisamment profonde peut déborder la pile, ce qui provoque le plantage du programme. L'itération, en revanche, ne modifie que les variables existantes, elle est donc généralement plus rapide et n'occupe qu'une quantité constante de mémoire. C'est donc une distinction très importante pour un développeur!
Dans les langages à récursivité d'appel (généralement les langages fonctionnels), le compilateur peut être en mesure d'optimiser les appels récursifs de telle manière qu'ils n'occupent qu'une quantité constante de mémoire. Dans ces langues, la distinction importante n'est pas l'itération vs la récursivité, mais la version non-tail-call-récursion et la itération.
Bottom line: Vous devez être en mesure de faire la différence, sinon votre programme plantera.
while
les boucles sont une forme de récursivité, voir par ex. la réponse acceptée à cette question . Ils correspondent à l'opérateur μ dans la théorie de la calculabilité (voir par exemple ici ).
Toutes les variations de boucles for
qui itèrent sur une plage de nombres, une collection finie, un tableau, etc., correspondent à une récursion primitive, voir par ex. ici et ici . Notez que les boucles for
de C, C++, Java, etc., sont en fait du sucre syntaxique pour une boucle while
et ne correspondent donc pas à la récursivité primitive. La boucle Pascal for
est un exemple de récursivité primitive.
Une différence importante est que la récursion primitive se termine toujours, tandis que la récursion généralisée (boucles [while
) peut ne pas se terminer.
[~ # ~] modifier [~ # ~]
Quelques clarifications concernant les commentaires et autres réponses. "La récursivité se produit quand une chose est définie en termes d'elle-même ou de son type." (voir wikipedia ). Donc,
Une boucle while est-elle intrinsèquement une récursivité?
Puisque vous pouvez définir une boucle while
en termes de lui-même
while p do c := if p then (c; while p do c))
alors, oui , une boucle while
est une forme de récursivité. Les fonctions récursives sont une autre forme de récursivité (un autre exemple de définition récursive). Les listes et les arbres sont d'autres formes de récursivité.
Une autre question implicitement supposée par de nombreuses réponses et commentaires est
Les boucles while et les fonctions récursives sont-elles équivalentes?
La réponse à cette question est non : Une boucle while
correspond à une fonction récursive de queue, où les variables accessibles par la boucle correspondent aux arguments de la fonction récursive implicite, mais, comme d'autres l'ont souligné, les fonctions non récursives ne peuvent pas être modélisées par une boucle while
sans utiliser une pile supplémentaire.
Ainsi, le fait qu'une "boucle while
est une forme de récursivité" ne contredit pas le fait que "certaines fonctions récursives ne peuvent pas être exprimées par une boucle while
".
Un appel de queue (ou appel récursif de queue) est exactement implémenté comme un "goto avec des arguments" (sans pousser aucun appel supplémentaire ) sur la pile d'appel ) et dans certains langages fonctionnels (Ocaml notamment) est la manière habituelle de boucler.
Ainsi, une boucle while (dans les langues en ayant) peut être considérée comme se terminant par un appel de queue à son corps (ou son test de tête).
De même, les appels récursifs ordinaires (sans appel de queue) peuvent être simulés par des boucles (en utilisant une pile).
Lisez aussi à propos de continuations et style de passage de continuation .
Donc, "récursivité" et "itération" sont profondément équivalentes.
Il est vrai que la récursivité et les boucles while non bornées sont équivalentes en termes d'expressivité de calcul. Autrement dit, tout programme écrit de manière récursive peut être réécrit dans un programme équivalent en utilisant des boucles à la place, et vice versa. Les deux approches sont turing-complete, c'est-à-dire que l'une ou l'autre peut être utilisée pour calculer n'importe quelle fonction calculable.
La différence fondamentale en termes de programmation est que la récursivité vous permet d'utiliser des données stockées sur la pile d'appels. Pour illustrer cela, supposez que vous souhaitez imprimer les éléments d'une liste à liaison unique en utilisant soit une boucle soit une récursivité. Je vais utiliser C pour l'exemple de code:
typedef struct List List;
struct List
{
List* next;
int element;
};
void print_list_loop(List* l)
{
List* it = l;
while(it != NULL)
{
printf("Element: %d\n", it->element);
it = it->next;
}
}
void print_list_rec(List* l)
{
if(l == NULL) return;
printf("Element: %d\n", l->element);
print_list_rec(l->next);
}
C'est simple, non? Maintenant, apportons une légère modification: imprimez la liste dans l'ordre inverse.
Pour la variante récursive, il s'agit d'une modification presque triviale de la fonction d'origine:
void print_list_reverse_rec(List* l)
{
if (l == NULL) return;
print_list_reverse_rec(l->next);
printf("Element: %d\n", l->element);
}
Cependant, pour la fonction de boucle, nous avons un problème. Notre liste est liée individuellement et ne peut donc être parcourue que vers l'avant. Mais puisque nous imprimons en sens inverse, nous devons commencer à imprimer le dernier élément. Une fois que nous avons atteint le dernier élément, nous ne pouvons plus revenir à l'avant-dernier élément.
Donc, soit nous devons faire beaucoup de re-traversée, soit nous devons construire une structure de données auxiliaire qui garde la trace des éléments visités et à partir de laquelle nous pouvons ensuite imprimer efficacement.
Pourquoi n'avons-nous pas ce problème avec la récursivité? Parce qu'en récursivité nous avons déjà une structure de données auxiliaire en place: la pile d'appels de fonction.
Puisque la récursivité nous permet de revenir à l'invocation précédente de l'appel récursif, avec toutes les variables locales et l'état de cet appel toujours intacts, nous gagnons une certaine flexibilité qui serait fastidieuse à modéliser dans le cas itératif.
Les boucles sont une forme spéciale de récursivité pour réaliser une tâche spécifique (principalement l'itération). On peut implémenter une boucle dans un style récursif avec les mêmes performances [1] dans plusieurs langues. et dans le SICP [2], vous pouvez voir que les boucles sont décrites comme du "sucre syntaxique". Dans la plupart des langages de programmation impératifs, les blocs for et while utilisent la même étendue que leur fonction parent. Néanmoins, dans la plupart des langages de programmation fonctionnels, il n'y a ni pour ni pendant que des boucles existent car elles ne sont pas nécessaires.
La raison pour laquelle les langages impératifs ont des boucles for/while est qu'ils gèrent les états en les mutant. Mais en fait, si vous regardez sous un angle différent, si vous pensez à un bloc while comme une fonction elle-même, en prenant un paramètre, en le traitant et en renvoyant un nouvel état - qui pourrait également être l'appel de la même fonction avec des paramètres différents - vous peut considérer la boucle comme une récursivité.
Le monde pourrait également être défini comme mutable ou immuable. si nous définissons le monde comme un ensemble de règles, et appelons une fonction ultime qui prend toutes les règles, et l'état actuel comme paramètres, et renvoyons le nouvel état en fonction de ces paramètres qui a la même fonctionnalité (générer l'état suivant dans le même façon), on pourrait aussi bien dire que c'est une récursivité et une boucle.
dans l'exemple suivant, la vie est la fonction prend deux paramètres "règles" et "état", et un nouvel état sera construit dans la prochaine fois.
life rules state = life rules new_state
where new_state = construct_state_in_time rules state
[1]: l'optimisation des appels de queue est une optimisation courante dans les langages de programmation fonctionnels pour utiliser la pile de fonctions existante dans les appels récursifs au lieu d'en créer une nouvelle.
[2]: Structure et interprétation des programmes informatiques, MIT. https://mitpress.mit.edu/books/structure-and-interpretation-computer-programs