Casi entiendo cómo funciona la recursividad de cola y la diferencia entre ella y una recursividad normal. Yo solamente no entiendo por qué no se requiere pila de recordar su dirección de retorno.
// tail recursion
int fac_times (int n, int acc) {
if (n == 0) return acc;
else return fac_times(n - 1, acc * n);
}
int factorial (int n) {
return fac_times (n, 1);
}
// normal recursion
int factorial (int n) {
if (n == 0) return 1;
else return n * factorial(n - 1);
}
No hay nada que hacer después de llamar a una función en sí misma en una función de recursión de cola, pero no tiene sentido para mí.
c
algorithm
recursion
tail-recursion
Alan Coromano
fuente
fuente
-O3
. El enlace es para una discusión anterior que cubre un terreno muy similar y discute lo que es necesario para implementar esta optimización.Respuestas:
El compilador simplemente puede transformar esto
en algo como esto:
fuente
Usted pregunta por qué "no requiere pila para recordar su dirección de retorno".
Me gustaría cambiar esto. Se hace uso de la pila de recordar la dirección de retorno. El truco es que la función en la que ocurre la recursión de cola tiene su propia dirección de retorno en la pila, y cuando salta a la función llamada, tratará esto como su propia dirección de retorno.
Concretamente, sin optimización de llamada de cola:
En este caso, cuando
g
se llama, la pila se verá así:Por otro lado, con la optimización de llamada de cola:
En este caso, cuando
g
se llama, la pila se verá así:Claramente, cuando
g
regrese, volverá a la ubicación desde dondef
se llamó.EDITAR : El ejemplo anterior usa el caso donde una función llama a otra función. El mecanismo es idéntico cuando la función se llama a sí misma.
fuente
El compilador puede transformar la recursividad de la cola en un bucle, especialmente cuando se usan acumuladores.
compilaría algo como
fuente
Hay dos elementos que deben estar presentes en una función recursiva:
Una función recursiva "regular" mantiene (2) en el marco de la pila.
Los valores de retorno en la función recursiva regular se componen de dos tipos de valores:
Veamos tu ejemplo:
El cuadro f (5) "almacena" el resultado de su propio cálculo (5) y el valor de f (4), por ejemplo. Si llamo factorial (5), justo antes de que las llamadas de la pila comiencen a colapsar, tengo:
Observe que cada pila almacena, además de los valores que mencioné, todo el alcance de la función. Entonces, el uso de memoria para una función recursiva f es O (x), donde x es el número de llamadas recursivas que tengo que hacer. Entonces, si necesito 1kb de RAM para calcular factorial (1) o factorial (2), necesito ~ 100k para calcular factorial (100), y así sucesivamente.
Una función recursiva de cola puso (2) en sus argumentos.
En una Recursión de cola, paso el resultado de los cálculos parciales en cada cuadro recursivo al siguiente usando parámetros. Veamos nuestro ejemplo factorial, Tail Recursive:
int factorial (int n) {int helper (int num, int acumulado) {if num == 0 return acumulado de lo contrario return helper (num - 1, acumulado * num)} return helper (n, 1)
}
Veamos sus marcos en factorial (4):
¿Ves las diferencias? En las llamadas recursivas "regulares", las funciones de retorno componen recursivamente el valor final. En Tail Recursion solo hacen referencia al caso base (el último evaluado) . Llamamos acumulador al argumento que realiza un seguimiento de los valores más antiguos.
Plantillas de recursión
La función recursiva regular es la siguiente:
Para transformarlo en una recursión de cola, nosotros:
Mira:
¿Ver la diferencia?
Tail Call optimization
Como no se está almacenando ningún estado en las pilas de Casos No Fronterizos de la Llamada de Cola, no son tan importantes. Algunos idiomas / intérpretes luego sustituyen la pila anterior por la nueva. Por lo tanto, sin marcos de pila que limiten el número de llamadas, las Llamadas de cola se comportan como un bucle for en estos casos.
Depende de su compilador optimizarlo o no.
fuente
Aquí hay un ejemplo simple que muestra cómo funcionan las funciones recursivas:
La recurrencia de cola es una función recursiva simple, donde la recurrencia se realiza al final de la función, por lo tanto, no se hace ningún código en ascendencia, lo que ayuda a la mayoría de los compiladores de lenguajes de programación de alto nivel a hacer lo que se conoce como Optimización de recursión de cola , también tiene optimización más compleja conocida como módulo de recursión de cola
fuente
La función recursiva es una función que llama por sí misma
Permite a los programadores escribir programas eficientes utilizando una cantidad mínima de código .
La desventaja es que pueden causar bucles infinitos y otros resultados inesperados si no se escriben correctamente .
Explicaré la función recursiva simple y la función recursiva de cola
Para escribir una función recursiva simple
Del ejemplo dado:
Del ejemplo anterior
Es el factor decisivo cuando salir del ciclo
¿Se debe realizar el procesamiento real?
Permítanme romper la tarea uno por uno para una fácil comprensión.
Veamos qué sucede internamente si corro
fact(4)
If
el bucle falla, por lo que pasa alelse
bucle y regresa4 * fact(3)
En la memoria de pila, tenemos
4 * fact(3)
Sustituyendo n = 3
If
el bucle falla, por lo que pasa alelse
bucleentonces regresa
3 * fact(2)
Recuerde que llamamos `` `4 * fact (3)` `
La salida para
fact(3) = 3 * fact(2)
Hasta ahora la pila tiene
4 * fact(3) = 4 * 3 * fact(2)
En la memoria de pila, tenemos
4 * 3 * fact(2)
Sustituyendo n = 2
If
el bucle falla, por lo que pasa alelse
bucleentonces regresa
2 * fact(1)
Recuerda que llamamos
4 * 3 * fact(2)
La salida para
fact(2) = 2 * fact(1)
Hasta ahora la pila tiene
4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)
En la memoria de pila, tenemos
4 * 3 * 2 * fact(1)
Sustituyendo n = 1
If
el bucle es verdaderoentonces regresa
1
Recuerda que llamamos
4 * 3 * 2 * fact(1)
La salida para
fact(1) = 1
Hasta ahora la pila tiene
4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1
Finalmente, el resultado del hecho (4) = 4 * 3 * 2 * 1 = 24
La recursión de la cola sería
If
el bucle falla, por lo que pasa alelse
bucle y regresafact(3, 4)
En la memoria de pila, tenemos
fact(3, 4)
Sustituyendo n = 3
If
el bucle falla, por lo que pasa alelse
bucleentonces regresa
fact(2, 12)
En la memoria de pila, tenemos
fact(2, 12)
Sustituyendo n = 2
If
el bucle falla, por lo que pasa alelse
bucleentonces regresa
fact(1, 24)
En la memoria de pila, tenemos
fact(1, 24)
Sustituyendo n = 1
If
el bucle es verdaderoentonces regresa
running_total
La salida para
running_total = 24
Finalmente, el resultado del hecho (4,1) = 24
fuente
Mi respuesta es más una conjetura, porque la recursividad es algo relacionado con la implementación interna.
En la recursividad de cola, la función recursiva se llama al final de la misma función. Probablemente el compilador puede optimizar de la siguiente manera:
Como puede ver, estamos terminando la función original antes de la próxima iteración de la misma función, por lo que en realidad no estamos "usando" la pila.
Pero creo que si hay destructores a los que se debe llamar dentro de la función, entonces esta optimización puede no aplicarse.
fuente
El compilador es lo suficientemente inteligente como para comprender la recursividad de cola. En el caso de que, al regresar de una llamada recursiva, no haya una operación pendiente y la última llamada sea recursiva, pertenece a la categoría de recursividad de cola. El compilador básicamente realiza la optimización de recursión de cola, eliminando la implementación de la pila. Considere debajo del código.
Después de realizar la optimización, el código anterior se convierte en uno inferior.
Así es como el compilador optimiza la recursividad de cola.
fuente