Question
Quels sont les moyens possibles pour résoudre un débordement de pile causé par un algorithme récursif?
Exemple
J'essaie de résoudre le problème 14 du projet Euler et j'ai décidé de l'essayer avec un algorithme récursif. Toutefois, le programme s’arrête avec une erreur java.lang.StackOverflowError. Naturellement. L'algorithme a en effet débordé de la pile car j'ai essayé de générer une séquence de Collatz pour un très grand nombre.
Solutions
Je me demandais donc: de quelle manière standard existe-t-il pour résoudre un débordement de pile en supposant que votre algorithme récursif a été écrit correctement et finirait toujours par déborder de la pile? Deux concepts qui me sont venus à l’esprit étaient:
- récursion de la queue
- itération
Les idées (1) et (2) sont-elles correctes? Y a-t-il d'autres options?
modifier
Il serait utile de voir du code, de préférence en Java, C #, Groovy ou Scala.
N'utilisez peut-être pas le problème de Project Euler mentionné ci-dessus pour ne pas gâcher d'autres, mais utilisez un autre algorithme. Factorielle peut-être, ou quelque chose de similaire.
la source
Réponses:
L'optimisation des appels en queue est présente dans de nombreux langages et compilateurs. Dans cette situation, le compilateur reconnaît une fonction de la forme:
Ici, le langage peut reconnaître que le résultat renvoyé est le résultat d'une autre fonction et transformer un appel de fonction avec un nouveau cadre de pile en saut.
Sachez que la méthode factorielle classique:
n’est pas optimisable en raison de l’inspection nécessaire lors du retour. ( Exemple de code source et de sortie compilée )
Pour rendre cet appel de queue optimisable,
Compiler ce code avec
gcc -O2 -S fact.c
(le -O2 est nécessaire pour permettre l'optimisation dans le compilateur, mais avec plus d'optimisations de -O3, il devient difficile pour un humain de lire ...)( Exemple de code source et de sortie compilée )
On peut voir dans segment
.L3
, lejne
plutôt que acall
(qui appelle un sous-programme avec un nouveau cadre de pile).Veuillez noter que cela a été fait avec C. L’optimisation des appels en Java est difficile et dépend de l’implémentation de la JVM (cela dit, je n’en ai pas vu qui le fasse, car c’est difficile et les implications du modèle de sécurité Java requis nécessitant des cadres de pile - ce que TCO évite) - tail-récursion + java et tail-récursion + optimisation sont de bons ensembles de balises à parcourir. Vous constaterez peut-être que d'autres langages de la machine virtuelle Java sont en mesure d'optimiser davantage la récursion de la queue (essayez clojure (ce qui nécessite l' optimisation de l'appel récurrent à la queue) ou scala).
Cela dit,
Il y a une certaine joie à savoir que vous avez écrit quelque chose de juste - de la manière idéale.
Et maintenant, je vais chercher du scotch et mettre de la musique électronique allemande ...
A la question générale de "méthodes pour éviter un débordement de pile dans un algorithme récursif" ...
Une autre approche consiste à inclure un compteur de récursivité. C’est davantage pour détecter des boucles infinies causées par des situations indépendantes de la volonté (et un mauvais codage).
Le compteur de récurrence prend la forme de
Chaque fois que vous passez un appel, vous incrémentez le compteur. Si le compteur devient trop gros, vous obtenez une erreur (ici, un retour de -1, mais dans d'autres langues, vous préférerez peut-être une exception). L'idée est d'empêcher que des choses pires se produisent (erreurs de mémoire insuffisante) lors d'une récursion beaucoup plus profonde que prévu et probablement une boucle infinie.
En théorie, vous ne devriez pas avoir besoin de ça. En pratique, j'ai vu du code mal écrit qui a heurté ce problème en raison d'une pléthore de petites erreurs et de mauvaises pratiques de codage (problèmes de concurrence multithread où quelque chose change quelque chose en dehors de la méthode qui fait passer un autre thread à une boucle infinie d'appels récursifs).
Utilisez le bon algorithme et résolvez le bon problème. Spécifiquement pour la conjecture de Collatz, il apparaît que vous essayez de la résoudre de la manière xkcd :
Vous commencez à un numéro et faites une traversée d’arbres. Cela conduit rapidement à un très grand espace de recherche. Une exécution rapide pour calculer le nombre d'itérations pour la réponse correcte aboutit à environ 500 étapes. Cela ne devrait pas poser de problème pour la récursivité avec un petit cadre de pile.
Bien que connaître la solution récursive ne soit pas une mauvaise chose, il faut également savoir que la solution itérative est souvent préférable . Un certain nombre de façons d’aborder la conversion d’un algorithme récursif en un algorithme itératif s’observe sur Stack Overflow at Way pour passer de la récursion à l’itération .
la source
Gardez à l'esprit que l'implémentation du langage doit prendre en charge l'optimisation de la récursion de la queue. Je ne pense pas que les principaux compilateurs Java le fassent.
La mémorisation signifie que vous vous souvenez du résultat d'un calcul plutôt que de le recalculer à chaque fois, comme ceci:
Lorsque vous calculez chaque séquence de moins d'un million, il y aura beaucoup de répétition à la fin des séquences. La mémoisation en fait une table de hachage rapide pour les valeurs précédentes au lieu de rendre la pile de plus en plus profonde.
la source
Je suis surpris que personne n'ait encore mentionné le trampoline . Un trampoline (en ce sens) est une boucle qui appelle de manière itérative des fonctions renvoyant des thunk (style de continuation continuation) et peut être utilisée pour implémenter des appels de fonction récursifs dans une langue de programmation orientée pile.
Cette question de StackOverflow donne plus de détails sur diverses implémentations du trampoline en Java: Gestion de StackOverflow en Java pour Trampoline
la source
Si vous utilisez un langage et un compilateur qui reconnaissent les fonctions récursives et les gèrent correctement (c.-à-d. "Remplace l'appelant en place avec l'appelé"), la pile ne devrait pas devenir incontrôlable. Cette optimisation réduit essentiellement une méthode récursive à une méthode itérative. Je ne pense pas que Java le fasse, mais je sais que Racket le fait.
Si vous optez pour une approche itérative plutôt que récursive, vous évacuez en grande partie le besoin de se souvenir d'où proviennent les appels et éliminez pratiquement le risque de débordement de pile (des appels récursifs de toute façon).
La mémorisation est excellente et peut réduire le nombre total d'appels de méthode en recherchant les résultats calculés précédemment dans une mémoire cache, étant donné que votre calcul global impliquera de nombreux calculs plus petits et répétés. Cette idée est géniale - elle est également indépendante du fait que vous utilisiez ou non une approche itérative ou récursive.
la source
vous pouvez créer une énumération qui remplacera la récursivité ... voici un exemple pour calculer le nombre de professeurs qui font cela ...
même s'il ne s'agit pas d'une mémoisation, vous éviterez ainsi un débordement de pile
MODIFIER
Je suis désolé si j'ai contrarié certains d'entre vous. Ma seule intention était de montrer comment éviter un débordement de pile. J'aurais probablement dû écrire un exemple de code complet au lieu d'un simple extrait de code brut rapidement écrit.
Le code suivant
... euh ... si vous l'exécutez, assurez-vous de configurer votre fenêtre d'interpréteur de commande pour avoir un tampon de 9999 lignes ... les 300 habituelles ne suffiront pas pour passer en revue les résultats du programme ci-dessous ...
Je déclare * 1 variable statique "instance" dans la classe Faculty à un magasin en singleton. De cette façon, tant que votre programme est en cours d'exécution, chaque fois que vous "GetInstance ()" de la classe, vous obtenez l'instance qui a stocké toutes les valeurs déjà calculées. * 1 SortedList statique qui contiendra toutes les valeurs déjà calculées
Dans le constructeur, j'ajoute également 2 valeurs spéciales de la liste 1 pour les entrées 0 et 1.
la source
Comme pour Scala, vous pouvez ajouter l’
@tailrec
annotation à une méthode récursive. De cette façon, le compilateur s'assure que l'optimisation de l'appel final a bien eu lieu:Donc, cela ne compilera pas (factoriel):
le message d'erreur est:
D'autre part:
compile, et l'optimisation des appels de queue a eu lieu.
la source
Une possibilité qui n’a pas encore été mentionnée est d’avoir une récursivité, mais sans utiliser de pile système. Bien sûr, vous pouvez également saturer votre segment de mémoire, mais si votre algorithme a vraiment besoin de revenir en arrière sous une forme ou une autre (pourquoi utiliser la récursion autrement?), Vous n'avez pas le choix.
Il existe des implémentations sans pile de certains langages, par exemple Stackless Python .
la source
Une autre solution serait de simuler votre propre pile et de ne pas compter sur l'implémentation du compilateur + runtime. Ce n'est pas une solution simple ni rapide, mais théoriquement, StackOverflow n'est disponible que lorsque vous manquez de mémoire.
la source