Optimización de llamadas de cola está presente en muchos idiomas y compiladores. En esta situación, el compilador reconoce una función de la forma:
int foo(n) {
...
return bar(n);
}
Aquí, el idioma puede reconocer que el resultado que se devuelve es el resultado de otra función y cambiar una llamada de función con un nuevo marco de pila en un salto.
Ten en cuenta que el método factorial clásico:
int factorial(n) {
if(n == 0) return 1;
if(n == 1) return 1;
return n * factorial(n - 1);
}
no es no llamada de cola optimizable debido a la inspección necesaria en la devolución. ( Ejemplo de código fuente y salida compilada )
Para hacer que esta llamada final sea optimizable,
int _fact(int n, int acc) {
if(n == 1) return acc;
return _fact(n - 1, acc * n);
}
int factorial(int n) {
if(n == 0) return 1;
return _fact(n, 1);
}
Compilando este código con gcc -O2 -S fact.c
(el -O2 es necesario para habilitar la optimización en el compilador, pero con más optimizaciones de -O3 se hace difícil que un humano lea ...)
_fact(int, int):
cmpl $1, %edi
movl %esi, %eax
je .L2
.L3:
imull %edi, %eax
subl $1, %edi
cmpl $1, %edi
jne .L3
.L2:
rep ret
( Código fuente de ejemplo y salida compilada )
Uno puede ver en el segmento .L3
, el jne
en lugar de un call
(lo que hace una llamada de subrutina con un nuevo marco de pila).
Tenga en cuenta que esto se hizo con C. La optimización de llamadas Tail en Java es difícil y depende de la implementación de JVM (es decir, no he visto ninguna que lo haga, porque es difícil y tiene implicaciones del modelo de seguridad Java requerido). que requieren marcos de pila, que es lo que TCO evita) - tail-recursion + java y tail-recursion + optimization son buenos conjuntos de etiquetas para navegar. Es posible que otros lenguajes JVM puedan optimizar la recursión de la cola (pruebe clojure (que requiere recurrir para optimizar la llamada a la cola), o scala).
Dicho esto,

Hayunaciertaalegríaalsaberqueescribistealgocorrecto,delamaneraidealenquepuedehacerse.
Yahora, Voy a tomar un poco de whisky y poner algo de electrónica alemana ...
A la pregunta general de "métodos para evitar un desbordamiento de pila en un algoritmo recursivo" ...
Otro enfoque es incluir un contador de recursión. Esto es más para detectar bucles infinitos causados por situaciones fuera del control (y codificación deficiente).
El contador de recursión toma la forma de
int foo(arg, counter) {
if(counter > RECURSION_MAX) { return -1; }
...
return foo(arg, counter + 1);
}
Cada vez que haces una llamada, incrementas el contador. Si el contador se vuelve demasiado grande, se produce un error (aquí, solo una devolución de -1, aunque en otros idiomas puede preferir lanzar una excepción). La idea es evitar que ocurran cosas peores (errores de memoria) cuando se realiza una recursión que es mucho más profunda de lo esperado y es probable que se produzca un bucle infinito.
En teoría, no deberías necesitar esto. En la práctica, he visto un código mal escrito que ha afectado a esto debido a una gran cantidad de pequeños errores y malas prácticas de codificación (problemas de concurrencia multiproceso donde algo cambia algo fuera del método que hace que otro hilo entre en un bucle infinito de llamadas recursivas).
Usa el algoritmo correcto y resuelve el problema correcto. Específicamente para la Conjetura de Collatz, aparece que está intentando resolverlo de la forma xkcd :

Estáscomenzandoconunnúmeroyhaciendounrecorridodeárbol.Estoconducerápidamenteaunespaciodebúsquedamuygrande.Unaejecuciónrápidaparacalcularelnúmerodeiteracionesparalosresultadosdelarespuestacorrectaenunos500pasos.Estonodeberíaserunproblemaparalarecursiónconunmarcodepilapequeño.
Aunquesaberquelasoluciónrecursivanoesalgomalo,tambiéndeberíadarsecuentadequemuchasveceslasolucióniterativa es mejor . Se pueden ver varias formas de acercar un algoritmo recursivo a uno iterativo en Desbordamiento de pila en Cómo pasar de la recursión a la iteración .