¿Qué es la recursividad de la cola?

1697

Al comenzar a aprender lisp, me he encontrado con el término recursivo de cola . ¿Qué significa exactamente?

Ben Lever
fuente
155
Para los curiosos: tanto tiempo como tiempo han estado en el idioma durante mucho tiempo. Mientras estaba en uso en inglés antiguo; while es un desarrollo de inglés medio de while. Como conjunciones tienen un significado intercambiable, pero no han sobrevivido en el inglés americano estándar.
Filip Bartuzi
14
Tal vez sea tarde, pero este es un artículo bastante bueno sobre el recursivo de la cola: programmerinterview.com/index.php/recursion/tail-recursion
Sam003
55
Uno de los grandes beneficios de identificar una función recursiva de cola es que se puede convertir en una forma iterativa y, por lo tanto, revivir el algoritmo de método-stack-gastos generales. Me gustaría visitar la respuesta de @Kyle Cronin y algunos otros a continuación
KGhatak
Este enlace de @yesudeep es la mejor y más detallada descripción que he encontrado - lua.org/pil/6.3.html
Jeff Fischer
1
¿Alguien podría decirme: la fusión y la ordenación rápida usan la recursividad de cola (TRO)?
majurageerthan

Respuestas:

1722

Considere una función simple que agrega los primeros N números naturales. (por ejemplo sum(5) = 1 + 2 + 3 + 4 + 5 = 15)

Aquí hay una implementación simple de JavaScript que utiliza la recursividad:

function recsum(x) {
    if (x === 1) {
        return x;
    } else {
        return x + recsum(x - 1);
    }
}

Si llamó recsum(5), esto es lo que evaluaría el intérprete de JavaScript:

recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + 1)))
15

Observe cómo cada llamada recursiva debe completarse antes de que el intérprete de JavaScript comience a hacer el trabajo de calcular la suma.

Aquí hay una versión recursiva de la cola de la misma función:

function tailrecsum(x, running_total = 0) {
    if (x === 0) {
        return running_total;
    } else {
        return tailrecsum(x - 1, running_total + x);
    }
}

Aquí está la secuencia de eventos que ocurrirían si llamaras tailrecsum(5), (lo que sería efectivamente tailrecsum(5, 0), debido al segundo argumento predeterminado).

tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15

En el caso recursivo de cola, con cada evaluación de la llamada recursiva, running_totalse actualiza.

Nota: La respuesta original utiliza ejemplos de Python. Estos se han cambiado a JavaScript, ya que los intérpretes de Python no admiten la optimización de llamadas de cola . Sin embargo, aunque la optimización de la cola de llamadas es parte de la especificación ECMAScript 2015 , la mayoría de los intérpretes de JavaScript no la admiten .

Lorin Hochstein
fuente
32
¿Puedo decir que con la recursión de cola la respuesta final se calcula por la ÚLTIMA invocación del método solo? Si NO es una recursión de cola, necesita todos los resultados para todos los métodos para calcular la respuesta.
chrisapotek
2
Aquí hay un apéndice que presenta algunos ejemplos en Lua: lua.org/pil/6.3.html ¡ Puede ser útil para revisar eso también! :)
yesudeep
2
¿Alguien puede responder la pregunta de chrisapotek? Estoy confundido sobre cómo tail recursionse puede lograr en un lenguaje que no optimiza las llamadas de cola.
Kevin Meredith
3
@KevinMeredith "recursión de cola" significa que la última instrucción en una función es una llamada recursiva a la misma función. Tienes razón en que no tiene sentido hacer esto en un lenguaje que no optimice esa recursión. Sin embargo, esta respuesta muestra el concepto (casi) correctamente. Hubiera sido más claramente una llamada de cola, si se omitiera el "else:". No cambiaría el comportamiento, pero colocaría la llamada de cola como una declaración independiente. Lo enviaré como una edición.
ToolmakerSteve
2
Entonces, en Python no hay ventaja porque con cada llamada a la función tailrecsum, se crea un nuevo marco de pila, ¿verdad?
Quazi Irfan
708

En la recursividad tradicional , el modelo típico es que primero realiza sus llamadas recursivas y luego toma el valor de retorno de la llamada recursiva y calcula el resultado. De esta manera, no obtiene el resultado de su cálculo hasta que haya regresado de cada llamada recursiva.

En la recursividad de cola , primero realiza sus cálculos y luego ejecuta la llamada recursiva, pasando los resultados de su paso actual al siguiente paso recursivo. Esto da como resultado que la última declaración tenga la forma de (return (recursive-function params)). Básicamente, el valor de retorno de cualquier paso recursivo dado es el mismo que el valor de retorno de la próxima llamada recursiva .

La consecuencia de esto es que una vez que esté listo para realizar su próximo paso recursivo, ya no necesita el marco de pila actual. Esto permite cierta optimización. De hecho, con un compilador de manera apropiada por escrito, nunca debe tener un desbordamiento de pila risita con una llamada recursiva de cola. Simplemente reutilice el marco de pila actual para el siguiente paso recursivo. Estoy bastante seguro de que Lisp hace esto.

henrebotha
fuente
17
"Estoy bastante seguro de que Lisp hace esto" - Scheme lo hace, pero Common Lisp no siempre lo hace.
Aaron
2
@Daniel "Básicamente, el valor de retorno de cualquier paso recursivo dado es el mismo que el valor de retorno de la próxima llamada recursiva". - No veo que este argumento sea verdadero para el fragmento de código publicado por Lorin Hochstein. ¿Puedes por favor elaborar?
Geek
8
@ Geek Esta es una respuesta muy tardía, pero eso es cierto en el ejemplo de Lorin Hochstein. El cálculo para cada paso se realiza antes de la llamada recursiva, en lugar de después. Como resultado, cada parada solo devuelve el valor directamente del paso anterior. La última llamada recursiva finaliza el cálculo y luego devuelve el resultado final sin modificar todo el camino de regreso a la pila de llamadas.
reirab
3
Scala sí, pero necesita el @tailrec especificado para aplicarlo.
SilentDirge
2
"De esta manera, no obtienes el resultado de tu cálculo hasta que hayas regresado de cada llamada recursiva". - tal vez no entendí esto, pero esto no es particularmente cierto para los lenguajes perezosos donde la recursión tradicional es la única forma de obtener un resultado sin llamar a todas las recursiones (por ejemplo, doblar una lista infinita de Bools con &&).
hasufell
206

Un punto importante es que la recursión de la cola es esencialmente equivalente al bucle. No es solo una cuestión de optimización del compilador, sino un hecho fundamental sobre la expresividad. Esto va en ambos sentidos: puede tomar cualquier bucle del formulario

while(E) { S }; return Q

donde Ey Qson expresiones y Ses una secuencia de enunciados, y convertirlo en una función recursiva de cola

f() = if E then { S; return f() } else { return Q }

Por supuesto, E, S, y Qtienen que ser definidas para calcular un valor interesante sobre algunas variables. Por ejemplo, la función de bucle

sum(n) {
  int i = 1, k = 0;
  while( i <= n ) {
    k += i;
    ++i;
  }
  return k;
}

es equivalente a las funciones recursivas de cola

sum_aux(n,i,k) {
  if( i <= n ) {
    return sum_aux(n,i+1,k+i);
  } else {
    return k;
  }
}

sum(n) {
  return sum_aux(n,1,0);
}

(Este "ajuste" de la función recursiva de la cola con una función con menos parámetros es un idioma funcional común).

Chris Conway
fuente
En la respuesta de @LorinHochstein entendí, según su explicación, que la recursividad de la cola se produce cuando la porción recursiva sigue a "Retorno", sin embargo, en la suya, la recursiva de la cola no lo es. ¿Estás seguro de que tu ejemplo se considera correctamente la recursividad de la cola?
CodyBugstein
1
@Imray La parte recursiva de la cola es la declaración "return sum_aux" dentro de sum_aux.
Chris Conway
1
@lmray: el código de Chris es esencialmente equivalente. El orden del if / then y el estilo de la prueba de limitación ... if x == 0 versus if (i <= n) ... no es algo para obsesionarse. El punto es que cada iteración pasa su resultado a la siguiente.
Taylor
else { return k; }se puede cambiar areturn k;
c0der
144

Este extracto del libro Programación en Lua muestra cómo hacer una recursión de cola adecuada (en Lua, pero también debería aplicarse a Lisp) y por qué es mejor.

Una llamada de cola [recursión de cola] es una especie de goto vestido como una llamada. Una llamada de cola ocurre cuando una función llama a otra como su última acción, por lo que no tiene nada más que hacer. Por ejemplo, en el siguiente código, la llamada a ges una llamada de cola:

function f (x)
  return g(x)
end

Después de las fllamadas g, no tiene nada más que hacer. En tales situaciones, el programa no necesita volver a la función de llamada cuando finaliza la función llamada. Por lo tanto, después de la llamada de cola, el programa no necesita mantener ninguna información sobre la función de llamada en la pila. ...

Debido a que una llamada de cola adecuada no utiliza espacio de pila, no hay límite en el número de llamadas de cola "anidadas" que puede hacer un programa. Por ejemplo, podemos llamar a la siguiente función con cualquier número como argumento; nunca desbordará la pila:

function foo (n)
  if n > 0 then return foo(n - 1) end
end

... Como dije antes, una llamada de cola es una especie de goto. Como tal, una aplicación bastante útil de llamadas de cola adecuadas en Lua es para programar máquinas de estado. Dichas aplicaciones pueden representar cada estado por una función; cambiar de estado es ir a (o llamar) una función específica. Como ejemplo, consideremos un simple juego de laberinto. El laberinto tiene varias habitaciones, cada una con hasta cuatro puertas: norte, sur, este y oeste. En cada paso, el usuario ingresa una dirección de movimiento. Si hay una puerta en esa dirección, el usuario va a la sala correspondiente; de lo contrario, el programa imprime una advertencia. El objetivo es pasar de una habitación inicial a una habitación final.

Este juego es una máquina de estado típica, donde la sala actual es el estado. Podemos implementar dicho laberinto con una función para cada habitación. Usamos llamadas de cola para movernos de una habitación a otra. Un pequeño laberinto con cuatro habitaciones podría verse así:

function room1 ()
  local move = io.read()
  if move == "south" then return room3()
  elseif move == "east" then return room2()
  else print("invalid move")
       return room1()   -- stay in the same room
  end
end

function room2 ()
  local move = io.read()
  if move == "south" then return room4()
  elseif move == "west" then return room1()
  else print("invalid move")
       return room2()
  end
end

function room3 ()
  local move = io.read()
  if move == "north" then return room1()
  elseif move == "east" then return room4()
  else print("invalid move")
       return room3()
  end
end

function room4 ()
  print("congratulations!")
end

Entonces, cuando haces una llamada recursiva como:

function x(n)
  if n==0 then return 0
  n= n-2
  return x(n) + 1
end

Esto no es recursivo de cola porque todavía tiene cosas que hacer (agregar 1) en esa función después de que se realiza la llamada recursiva. Si ingresa un número muy alto, probablemente provocará un desbordamiento de la pila.

Hoffmann
fuente
99
Esta es una gran respuesta porque explica las implicaciones de las llamadas de cola sobre el tamaño de la pila.
Andrew Swan
@AndrewSwan De hecho, aunque creo que el autor de la pregunta original y el lector ocasional que podrían tropezar con esta pregunta podrían recibir un mejor servicio con la respuesta aceptada (ya que es posible que no sepa cuál es realmente la pila). Por cierto, uso Jira, grande ventilador.
Hoffmann
1
Mi respuesta favorita también debido a la inclusión de la implicación para el tamaño de la pila.
njk2015
80

Usando la recursividad regular, cada llamada recursiva empuja otra entrada en la pila de llamadas. Cuando se completa la recursividad, la aplicación tiene que abrir cada entrada completamente hacia abajo.

Con la recursividad de cola, dependiendo del idioma, el compilador puede colapsar la pila a una entrada, por lo que ahorra espacio de pila ... Una consulta recursiva grande en realidad puede causar un desbordamiento de pila.

Básicamente, las recursiones de cola pueden optimizarse en iteración.

FlySwat
fuente
1
"Una consulta recursiva grande en realidad puede causar un desbordamiento de la pila". debería estar en el primer párrafo, no en el segundo (recursión de cola) La gran ventaja de la recursión de la cola es que puede optimizarse (por ejemplo, Scheme) de tal manera que no se "acumulen" llamadas en la pila, por lo que se evitarán los desbordamientos de la pila.
Olivier Dulac
70

El archivo de la jerga tiene esto que decir sobre la definición de recursión de cola:

recursión de cola /n./

Si aún no está harto de esto, vea la recursión de la cola.

Palmadita
fuente
68

En lugar de explicarlo con palabras, aquí hay un ejemplo. Esta es una versión de esquema de la función factorial:

(define (factorial x)
  (if (= x 0) 1
      (* x (factorial (- x 1)))))

Aquí hay una versión de factorial que es recursiva de cola:

(define factorial
  (letrec ((fact (lambda (x accum)
                   (if (= x 0) accum
                       (fact (- x 1) (* accum x))))))
    (lambda (x)
      (fact x 1))))

Notará en la primera versión que la llamada recursiva al hecho se alimenta a la expresión de multiplicación y, por lo tanto, el estado debe guardarse en la pila al hacer la llamada recursiva. En la versión recursiva de cola no hay otra expresión S esperando el valor de la llamada recursiva, y como no hay más trabajo por hacer, el estado no tiene que guardarse en la pila. Como regla general, las funciones recursivas de cola de Scheme usan espacio de pila constante.

Kyle Cronin
fuente
44
+1 por mencionar el aspecto más importante de las recursiones de cola que pueden convertirse en una forma iterativa y, por lo tanto, convertirlo en una forma de complejidad de memoria O (1).
KGhatak
1
@KGhatak no exactamente; la respuesta habla correctamente de "espacio de pila constante", no de memoria en general. no ser quisquilloso, solo para asegurarse de que no haya malentendidos. por ejemplo, el list-reverseprocedimiento de recursivo de cola y mutación de cola se ejecutará en un espacio de pila constante, pero creará y ampliará una estructura de datos en el montón. Un recorrido de árbol podría usar una pila simulada, en un argumento adicional. etc.
Will Ness
45

La recursividad de cola se refiere a que la llamada recursiva es la última en la última instrucción lógica en el algoritmo recursivo.

Por lo general, en la recursión, tiene un caso base que es lo que detiene las llamadas recursivas y comienza a reventar la pila de llamadas. Para usar un ejemplo clásico, aunque más C-ish que Lisp, la función factorial ilustra la recursividad de la cola. La llamada recursiva ocurre después de verificar la condición del caso base.

factorial(x, fac=1) {
  if (x == 1)
     return fac;
   else
     return factorial(x-1, x*fac);
}

La llamada inicial a factorial sería factorial(n)donde fac=1(valor predeterminado) yn es el número para el que se calculará el factorial.

Peter Meyer
fuente
Encontré su explicación más fácil de entender, pero si se trata de algo, la recursión de cola solo es útil para funciones con casos base de una declaración. Considere un método como este postimg.cc/5Yg3Cdjn . Nota: el exterior elsees el paso que podría llamar un "caso base" pero abarca varias líneas. ¿Te estoy malentendiendo o mi suposición es correcta? ¿La recursión de la cola solo es buena para un revestimiento?
Quiero respuestas el
2
@IWantAnswers: no, el cuerpo de la función puede ser arbitrariamente grande. Todo lo que se requiere para una llamada de cola es que la rama en la que se encuentra llama a la función como lo último que hace, y devuelve el resultado de llamar a la función. El factorialejemplo es solo el clásico ejemplo simple, eso es todo.
TJ Crowder
28

Significa que, en lugar de tener que presionar el puntero de instrucciones en la pila, simplemente puede saltar a la parte superior de una función recursiva y continuar la ejecución. Esto permite que las funciones se repitan indefinidamente sin desbordar la pila.

Escribí una publicación de blog sobre el tema, que tiene ejemplos gráficos de cómo se ven los marcos de la pila.

Chris Smith
fuente
21

Aquí hay un fragmento de código rápido que compara dos funciones. El primero es la recursión tradicional para encontrar el factorial de un número dado. El segundo usa la recursión de la cola.

Muy simple e intuitivo de entender.

Una manera fácil de saber si una función recursiva es una cola recursiva es si devuelve un valor concreto en el caso base. Lo que significa que no devuelve 1 o verdadero o algo así. Lo más probable es que devuelva alguna variante de uno de los parámetros del método.

Otra forma es saber si la llamada recursiva está libre de cualquier adición, aritmética, modificación, etc., lo que significa que no es más que una llamada recursiva pura.

public static int factorial(int mynumber) {
    if (mynumber == 1) {
        return 1;
    } else {            
        return mynumber * factorial(--mynumber);
    }
}

public static int tail_factorial(int mynumber, int sofar) {
    if (mynumber == 1) {
        return sofar;
    } else {
        return tail_factorial(--mynumber, sofar * mynumber);
    }
}
AbuZubair
fuente
3
0! es 1. Entonces "mynumber == 1" debería ser "mynumber == 0".
polerto
19

La mejor manera para que yo entienda tail call recursiones un caso especial de recursión donde la última llamada (o la llamada de cola) es la función misma.

Comparando los ejemplos proporcionados en Python:

def recsum(x):
 if x == 1:
  return x
 else:
  return x + recsum(x - 1)

^ RECURSIÓN

def tailrecsum(x, running_total=0):
  if x == 0:
    return running_total
  else:
    return tailrecsum(x - 1, running_total + x)

^ RECURSIÓN DE LA COLA

Como puede ver en la versión recursiva general, la última llamada en el bloque de código es x + recsum(x - 1). Entonces, después de llamar al recsummétodo, hay otra operación que es x + ...

Sin embargo, en la versión recursiva de cola, la llamada final (o la llamada de cola) en el bloque de código es lo tailrecsum(x - 1, running_total + x)que significa que la última llamada se realiza al método en sí y no se realiza ninguna operación después de eso.

Este punto es importante porque la recursión de cola como se ve aquí no está haciendo crecer la memoria porque cuando la VM subyacente ve una función que se llama a sí misma en una posición de cola (la última expresión que se evaluará en una función), elimina el marco de pila actual, que se conoce como Tail Call Optimization (TCO).

EDITAR

NÓTESE BIEN. Tenga en cuenta que el ejemplo anterior está escrito en Python cuyo tiempo de ejecución no admite TCO. Este es solo un ejemplo para explicar el punto. TCO es compatible con idiomas como Scheme, Haskell, etc.

Abhiroop Sarkar
fuente
12

En Java, aquí hay una posible implementación recursiva de la cola de la función Fibonacci:

public int tailRecursive(final int n) {
    if (n <= 2)
        return 1;
    return tailRecursiveAux(n, 1, 1);
}

private int tailRecursiveAux(int n, int iter, int acc) {
    if (iter == n)
        return acc;
    return tailRecursiveAux(n, ++iter, acc + iter);
}

Compare esto con la implementación recursiva estándar:

public int recursive(final int n) {
    if (n <= 2)
        return 1;
    return recursive(n - 1) + recursive(n - 2);
}
jorgetown
fuente
1
Esto me devuelve resultados incorrectos, para la entrada 8 obtengo 36, tiene que ser 21. ¿Me estoy perdiendo algo? Estoy usando Java y copiarlo pegado.
Alberto Zaccagni
1
Esto devuelve SUM (i) para i en [1, n]. Nada que ver con Fibbonacci. Para un Fibbo, necesita una prueba que resta itera acccuándo iter < (n-1).
Askolein
10

No soy un programador de Lisp, pero creo que esto ayudará.

Básicamente es un estilo de programación tal que la llamada recursiva es lo último que haces.

Matt Hamilton
fuente
10

Aquí hay un ejemplo de Common Lisp que hace factoriales usando la recursión de cola. Debido a la naturaleza sin pila, uno podría realizar cálculos factoriales increíblemente grandes ...

(defun ! (n &optional (product 1))
    (if (zerop n) product
        (! (1- n) (* product n))))

Y luego, por diversión, podrías probar (format nil "~R" (! 25))

usuario922475
fuente
9

En resumen, una recursión de cola tiene la llamada recursiva como la última instrucción en la función para que no tenga que esperar la llamada recursiva.

Entonces, esta es una recursión de cola, es decir, N (x - 1, p * x) es la última declaración en la función en la que el compilador es inteligente para descubrir que puede optimizarse para un bucle for (factorial). El segundo parámetro p lleva el valor del producto intermedio.

function N(x, p) {
   return x == 1 ? p : N(x - 1, p * x);
}

Esta es la forma no recursiva de escribir la función factorial anterior (aunque algunos compiladores de C ++ pueden optimizarla de todos modos).

function N(x) {
   return x == 1 ? 1 : x * N(x - 1);
}

pero esto no es:

function F(x) {
  if (x == 1) return 0;
  if (x == 2) return 1;
  return F(x - 1) + F(x - 2);
}

Escribí una larga publicación titulada " Comprender la recursión de la cola - Visual Studio C ++ - Vista de ensamblaje "

ingrese la descripción de la imagen aquí

SteakOverCooked
fuente
1
¿Cómo es su función N recursiva de cola?
Fabian Pijcke
N (x-1) es la última instrucción en la función en la que el compilador es inteligente para darse cuenta de que puede optimizarse para un bucle for (factorial)
doctorlai
Mi preocupación es que su función N es exactamente la función recsum de la respuesta aceptada de este tema (excepto que es una suma y no un producto), y que se dice que recsum no es recursivo.
Fabian Pijcke
8

Aquí hay una versión de Perl 5 de la tailrecsumfunción mencionada anteriormente.

sub tail_rec_sum($;$){
  my( $x,$running_total ) = (@_,0);

  return $running_total unless $x;

  @_ = ($x-1,$running_total+$x);
  goto &tail_rec_sum; # throw away current stack frame
}
Brad Gilbert
fuente
8

Este es un extracto de Estructura e Interpretación de Programas de Computadora sobre la recursividad de la cola.

Al contrastar la iteración y la recursión, debemos tener cuidado de no confundir la noción de un proceso recursivo con la noción de un procedimiento recursivo. Cuando describimos un procedimiento como recursivo, nos estamos refiriendo al hecho sintáctico de que la definición del procedimiento se refiere (directa o indirectamente) al procedimiento mismo. Pero cuando describimos un proceso como siguiendo un patrón que es, por ejemplo, linealmente recursivo, estamos hablando de cómo evoluciona el proceso, no de la sintaxis de cómo se escribe un procedimiento. Puede parecer inquietante que nos refiramos a un procedimiento recursivo como el hecho de generar un proceso iterativo. Sin embargo, el proceso realmente es iterativo: su estado es capturado completamente por sus tres variables de estado, y un intérprete necesita hacer un seguimiento de solo tres variables para ejecutar el proceso.

Una razón por la cual la distinción entre proceso y procedimiento puede ser confusa es que la mayoría de las implementaciones de lenguajes comunes (incluidos Ada, Pascal y C) están diseñadas de tal manera que la interpretación de cualquier procedimiento recursivo consume una cantidad de memoria que crece con el número de llamadas a procedimientos, incluso cuando el proceso descrito es, en principio, iterativo. Como consecuencia, estos lenguajes pueden describir procesos iterativos solo recurriendo a "construcciones de bucle" de propósito especial como hacer, repetir, hasta, para y mientras. La implementación de Scheme no comparte este defecto. Ejecutará un proceso iterativo en espacio constante, incluso si el proceso iterativo se describe mediante un procedimiento recursivo. Una implementación con esta propiedad se llama tail-recursive. Con una implementación recursiva de cola, la iteración se puede expresar utilizando el mecanismo de llamada a procedimiento ordinario, de modo que las construcciones de iteración especiales son útiles solo como azúcar sintáctica.

ayushgp
fuente
1
Leí todas las respuestas aquí y, sin embargo, esta es la explicación más clara que toca el núcleo realmente profundo de este concepto. Lo explica de una manera tan directa que hace que todo se vea tan simple y tan claro. Perdona mi grosería por favor. De alguna manera me hace sentir que las otras respuestas simplemente no dan en el clavo. Creo que es por eso que importa SICP.
englealuze
8

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é tanto la función recursiva simple como la función recursiva de cola

Para escribir una función recursiva simple

  1. El primer punto a considerar es cuándo debe decidir salir del bucle, que es el bucle if
  2. El segundo es qué proceso hacer si somos nuestra propia función.

Del ejemplo dado:

public static int fact(int n){
  if(n <=1)
     return 1;
  else 
     return n * fact(n-1);
}

Del ejemplo anterior

if(n <=1)
     return 1;

Es el factor decisivo cuando salir del ciclo

else 
     return n * fact(n-1);

¿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)

  1. Sustituyendo n = 4
public static int fact(4){
  if(4 <=1)
     return 1;
  else 
     return 4 * fact(4-1);
}

Ifel bucle falla, por lo que pasa al elsebucle y regresa4 * fact(3)

  1. En la memoria de pila, tenemos 4 * fact(3)

    Sustituyendo n = 3

public static int fact(3){
  if(3 <=1)
     return 1;
  else 
     return 3 * fact(3-1);
}

Iflazo de falla por lo que va a elsebucle

entonces 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)

  1. En la memoria de pila, tenemos 4 * 3 * fact(2)

    Sustituyendo n = 2

public static int fact(2){
  if(2 <=1)
     return 1;
  else 
     return 2 * fact(2-1);
}

Iflazo de falla por lo que va a elsebucle

entonces 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)

  1. En la memoria de pila, tenemos 4 * 3 * 2 * fact(1)

    Sustituyendo n = 1

public static int fact(1){
  if(1 <=1)
     return 1;
  else 
     return 1 * fact(1-1);
}

If el bucle es verdadero

entonces 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

ingrese la descripción de la imagen aquí

La recursión de la cola sería

public static int fact(x, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(x-1, running_total*x);
    }
}

  1. Sustituyendo n = 4
public static int fact(4, running_total=1) {
    if (x==1) {
        return running_total;
    } else {
        return fact(4-1, running_total*4);
    }
}

Ifel bucle falla, por lo que pasa al elsebucle y regresafact(3, 4)

  1. En la memoria de pila, tenemos fact(3, 4)

    Sustituyendo n = 3

public static int fact(3, running_total=4) {
    if (x==1) {
        return running_total;
    } else {
        return fact(3-1, 4*3);
    }
}

Iflazo de falla por lo que va a elsebucle

entonces regresa fact(2, 12)

  1. En la memoria de pila, tenemos fact(2, 12)

    Sustituyendo n = 2

public static int fact(2, running_total=12) {
    if (x==1) {
        return running_total;
    } else {
        return fact(2-1, 12*2);
    }
}

Iflazo de falla por lo que va a elsebucle

entonces regresa fact(1, 24)

  1. En la memoria de pila, tenemos fact(1, 24)

    Sustituyendo n = 1

public static int fact(1, running_total=24) {
    if (x==1) {
        return running_total;
    } else {
        return fact(1-1, 24*1);
    }
}

If el bucle es verdadero

entonces regresa running_total

La salida para running_total = 24

Finalmente, el resultado del hecho (4,1) = 24

ingrese la descripción de la imagen aquí

Nursnaaz
fuente
7

La recursión de la cola es la vida que estás viviendo ahora. Recicla constantemente el mismo marco de pila, una y otra vez, porque no hay razón ni medios para volver a un marco "anterior". El pasado ha terminado y terminado para que pueda ser descartado. Obtienes un cuadro, siempre avanzando hacia el futuro, hasta que tu proceso inevitablemente muera.

La analogía se rompe cuando considera que algunos procesos pueden utilizar marcos adicionales, pero aún se consideran recursivos si la pila no crece infinitamente.

Gracias
fuente
1
no se rompe bajo la interpretación del trastorno de personalidad dividida :) Una sociedad de la mente; una mente como sociedad. :)
Will Ness
¡Guauu! Ahora esa es otra forma de pensarlo
sutanu dalui
7

Una recursividad de cola es una función recursiva donde la función se llama a sí misma al final ("cola") de la función en la que no se realiza ningún cálculo después del retorno de la llamada recursiva. Muchos compiladores optimizan para cambiar una llamada recursiva a una cola recursiva o una llamada iterativa.

Considere el problema de calcular el factorial de un número.

Un enfoque directo sería:

  factorial(n):

    if n==0 then 1

    else n*factorial(n-1)

Supongamos que llama factorial (4). El árbol de recursión sería:

       factorial(4)
       /        \
      4      factorial(3)
     /             \
    3          factorial(2)
   /                  \
  2                factorial(1)
 /                       \
1                       factorial(0)
                            \
                             1    

La profundidad máxima de recursión en el caso anterior es O (n).

Sin embargo, considere el siguiente ejemplo:

factAux(m,n):
if n==0  then m;
else     factAux(m*n,n-1);

factTail(n):
   return factAux(1,n);

El árbol de recursión de hecho Cola (4) sería:

factTail(4)
   |
factAux(1,4)
   |
factAux(4,3)
   |
factAux(12,2)
   |
factAux(24,1)
   |
factAux(24,0)
   |
  24

Aquí también, la profundidad máxima de recursión es O (n) pero ninguna de las llamadas agrega ninguna variable adicional a la pila. Por lo tanto, el compilador puede eliminar una pila.

coding_ninza
fuente
7

La recursión de la cola es bastante rápida en comparación con la recursividad normal. Es rápido porque la salida de la llamada de los antepasados ​​no se escribirá en la pila para mantener la pista. Pero en la recursividad normal, todos los ancestros llaman a la salida escrita en la pila para mantener la pista.

Rohit Garg
fuente
6

Una función recursiva de cola es una función recursiva donde la última operación que realiza antes de regresar es realizar la llamada a la función recursiva. Es decir, el valor de retorno de la llamada de función recursiva se devuelve inmediatamente. Por ejemplo, su código se vería así:

def recursiveFunction(some_params):
    # some code here
    return recursiveFunction(some_args)
    # no code after the return statement

Los compiladores e intérpretes que implementan la optimización de llamadas de cola o la eliminación de llamadas de cola pueden optimizar el código recursivo para evitar desbordamientos de pila. Si su compilador o intérprete no implementa la optimización de llamadas de cola (como el intérprete CPython), no hay ningún beneficio adicional al escribir su código de esta manera.

Por ejemplo, esta es una función factorial recursiva estándar en Python:

def factorial(number):
    if number == 1:
        # BASE CASE
        return 1
    else:
        # RECURSIVE CASE
        # Note that `number *` happens *after* the recursive call.
        # This means that this is *not* tail call recursion.
        return number * factorial(number - 1)

Y esta es una versión recursiva de la función factorial:

def factorial(number, accumulator=1):
    if number == 0:
        # BASE CASE
        return accumulator
    else:
        # RECURSIVE CASE
        # There's no code after the recursive call.
        # This is tail call recursion:
        return factorial(number - 1, number * accumulator)
print(factorial(5))

(Tenga en cuenta que aunque se trata de código Python, el intérprete de CPython no realiza la optimización de las llamadas de cola, por lo que organizar su código de esta manera no confiere ningún beneficio en tiempo de ejecución).

Es posible que tenga que hacer que su código sea un poco más ilegible para utilizar la optimización de la llamada de cola, como se muestra en el ejemplo factorial. (Por ejemplo, el caso base ahora es poco intuitivo y el accumulatorparámetro se usa efectivamente como una especie de variable global).

Pero el beneficio de la optimización de llamadas de cola es que evita errores de desbordamiento de pila. (Notaré que puede obtener este mismo beneficio utilizando un algoritmo iterativo en lugar de uno recursivo).

Los desbordamientos de la pila se producen cuando la pila de llamadas ha introducido demasiados objetos de marco. Un objeto de marco se inserta en la pila de llamadas cuando se llama a una función, y se saca de la pila de llamadas cuando la función regresa. Los objetos de marco contienen información como variables locales y a qué línea de código volver cuando regrese la función.

Si su función recursiva realiza demasiadas llamadas recursivas sin regresar, la pila de llamadas puede exceder su límite de objeto de marco. (El número varía según la plataforma; en Python son 1000 objetos de marco por defecto). Esto causa un error de desbordamiento de pila . (¡Hola, de ahí viene el nombre de este sitio web!)

Sin embargo, si lo último que hace su función recursiva es hacer la llamada recursiva y devolver su valor de retorno, entonces no hay razón para que deba mantener el objeto de marco actual para permanecer en la pila de llamadas. Después de todo, si no hay código después de la llamada a la función recursiva, no hay razón para aferrarse a las variables locales del objeto de marco actual. Por lo tanto, podemos deshacernos del objeto de marco actual inmediatamente en lugar de mantenerlo en la pila de llamadas. El resultado final de esto es que su pila de llamadas no crece en tamaño y, por lo tanto, no puede apilar el desbordamiento.

Un compilador o un intérprete debe tener la optimización de la llamada de cola como una característica para poder reconocer cuándo se puede aplicar la optimización de la llamada de cola. Incluso entonces, es posible que tenga que reorganizar el código en su función recursiva para hacer uso de la optimización de la cola de llamadas, y depende de usted si esta disminución potencial en la legibilidad vale la pena.

rev. Al Sweigart
fuente
"Recurrencia de cola (también llamada optimización de llamada de cola o eliminación de llamada de cola)". No; la eliminación de la cola o la optimización de la cola es algo que puede aplicar a una función recursiva de cola, pero no son lo mismo. Puede escribir funciones recursivas de cola en Python (como muestra), pero no son más eficientes que una función no recursiva de cola, porque Python no realiza la optimización de llamadas de cola.
chepner el
¿Significa que si alguien logra optimizar el sitio web y hacer que la llamada recursiva sea recursiva, ya no tendremos el sitio StackOverflow? Esto es horrible.
Nadjib Mami
5

Para comprender algunas de las principales diferencias entre la recursividad de llamada de cola y la recursión de llamada no de cola, podemos explorar las implementaciones .NET de estas técnicas.

Aquí hay un artículo con algunos ejemplos en C #, F # y C ++ \ CLI: Adventures in Tail Recursion en C #, F # y C ++ \ CLI .

C # no se optimiza para la recursividad de llamadas de cola mientras que F # sí.

Las diferencias de principio implican bucles versus cálculo de Lambda. C # está diseñado con bucles en mente, mientras que F # está construido a partir de los principios del cálculo Lambda. Para un libro muy bueno (y gratuito) sobre los principios del cálculo Lambda, vea Estructura e interpretación de programas de computadora, por Abelson, Sussman y Sussman .

Con respecto a las llamadas de cola en F #, para un artículo introductorio muy bueno, vea Introducción detallada a las llamadas de cola en F # . Por último, aquí está un artículo que cubre la diferencia entre la recursividad no cola y la repetición de llamada final (en Fa #): Cola-recursividad frente a no recursión de cola en fa sostenido .

Si desea leer acerca de algunas de las diferencias de diseño de la recursividad de llamada de cola entre C # y F #, consulte Generar código de operación de llamada de cola en C # y F # .

Si le interesa lo suficiente como para querer saber qué condiciones impiden que el compilador de C # realice optimizaciones de llamada de cola, consulte este artículo: Condiciones de llamada de cola JIT CLR .

devinbost
fuente
4

Hay dos tipos básicos de recursiones: recidiva de la cabeza y recidiva de la cola.

En la recursividad principal , una función realiza su llamada recursiva y luego realiza algunos cálculos más, tal vez utilizando el resultado de la llamada recursiva, por ejemplo.

En una función recursiva de cola , todos los cálculos suceden primero y la llamada recursiva es lo último que sucede.

Tomado de esta publicación súper increíble. Por favor considere leerlo.

Abdullah Khan
fuente
4

Recursión significa una función que se llama a sí misma. Por ejemplo:

(define (un-ended name)
  (un-ended 'me)
  (print "How can I get here?"))

La recursión de cola significa la recursividad que concluye la función:

(define (un-ended name)
  (print "hello")
  (un-ended 'me))

Mira, lo último que hace la función sin terminar (procedimiento, en la jerga de Scheme) es llamarse a sí mismo. Otro ejemplo (más útil) es:

(define (map lst op)
  (define (helper done left)
    (if (nil? left)
        done
        (helper (cons (op (car left))
                      done)
                (cdr left))))
  (reverse (helper '() lst)))

En el procedimiento auxiliar, lo ÚLTIMO que hace si la izquierda no es nula es llamarse a sí mismo (DESPUÉS de contras algo y cdr algo). Esto es básicamente cómo mapear una lista.

La recursividad de cola tiene una gran ventaja de que el intérprete (o compilador, dependiendo del idioma y el proveedor) puede optimizarla y transformarla en algo equivalente a un ciclo while. De hecho, en la tradición de Scheme, la mayoría de los bucles "for" y "while" se realizan de manera recursiva (no hay un for y while, que yo sepa).

magice
fuente
3

Esta pregunta tiene muchas respuestas excelentes ... pero no puedo evitar intervenir con una versión alternativa de cómo definir "recursión de cola", o al menos "recursión de cola adecuada". A saber: ¿debería uno verlo como una propiedad de una expresión particular en un programa? ¿O debería uno verlo como una propiedad de una implementación de un lenguaje de programación ?

Para más información sobre este último punto de vista, hay un artículo clásico de Will Clinger, "Proper Tail Recursion and Space Efficiency" (PLDI 1998), que definió la "adecuada recursión de la cola" como una propiedad de una implementación de lenguaje de programación. La definición se construye para permitir que uno ignore los detalles de implementación (como si la pila de llamadas está realmente representada a través de la pila de tiempo de ejecución o mediante una lista de tramas vinculadas asignadas al montón).

Para lograr esto, utiliza el análisis asintótico: no del tiempo de ejecución del programa como generalmente se ve, sino del uso del espacio del programa . De esta manera, el uso de espacio de una lista vinculada asignada en el montón frente a una pila de llamadas en tiempo de ejecución termina siendo asintóticamente equivalente; así que uno puede ignorar ese detalle de implementación del lenguaje de programación (un detalle que ciertamente importa bastante en la práctica, pero puede enturbiar las aguas un poco cuando se intenta determinar si una implementación dada cumple con el requisito de ser "recursiva de propiedad" )

El trabajo merece un estudio cuidadoso por varias razones:

  • Ofrece una definición inductiva de las expresiones de cola y las llamadas de cola de un programa. (Tal definición, y por qué tales llamadas son importantes, parece ser el tema de la mayoría de las otras respuestas dadas aquí).

    Aquí están esas definiciones, solo para dar una idea del texto:

    Definición 1 Las expresiones de cola de un programa escrito en Core Scheme se definen inductivamente de la siguiente manera.

    1. El cuerpo de una expresión lambda es una expresión de cola
    2. Si (if E0 E1 E2)es una expresión de la cola, entonces ambos E1y E2son expresiones de la cola.
    3. Nada más es una expresión de cola.

    Definición 2 Una llamada de cola es una expresión de cola que es una llamada de procedimiento.

(una llamada recursiva de cola, o como dice el documento, "llamada de cola propia" es un caso especial de una llamada de cola donde se invoca el procedimiento).

  • Proporciona definiciones formales para seis "máquinas" diferentes para evaluar el Esquema Central, donde cada máquina tiene el mismo comportamiento observable, excepto la clase de complejidad espacial asintótica en la que se encuentra cada una.

    Por ejemplo, después de dar definiciones para máquinas con, respectivamente, 1. gestión de memoria basada en pila, 2. recolección de basura pero sin llamadas de cola, 3. recolección de basura y llamadas de cola, el documento continúa con estrategias de administración de almacenamiento aún más avanzadas, como 4. "evlis tail recursion", donde el entorno no necesita ser preservado a través de la evaluación del último argumento de subexpresión en una llamada de cola, 5. reduciendo el entorno de un cierre a solo las variables libres de ese cierre, y 6. La llamada semántica "segura para el espacio" tal como la definen Appel y Shao .

  • Para demostrar que las máquinas realmente pertenecen a seis clases distintas de complejidad espacial, el documento, para cada par de máquinas en comparación, proporciona ejemplos concretos de programas que expondrán la explosión espacial asintótica en una máquina pero no en la otra.


(Leyendo mi respuesta ahora, no estoy seguro si logré capturar realmente los puntos cruciales del artículo de Clinger . Pero, por desgracia, no puedo dedicar más tiempo a desarrollar esta respuesta en este momento).

pnkfelix
fuente
1

Muchas personas ya han explicado la recursividad aquí. Me gustaría citar un par de reflexiones sobre algunas ventajas que ofrece la recursividad del libro "Concurrencia en .NET, patrones modernos de programación concurrente y paralela" de Riccardo Terrell:

“La recursión funcional es la forma natural de iterar en FP porque evita la mutación de estado. Durante cada iteración, se pasa un nuevo valor al constructor del bucle para que se actualice (mute). Además, se puede componer una función recursiva, lo que hace que su programa sea más modular, así como también presenta oportunidades para explotar la paralelización ".

Aquí también hay algunas notas interesantes del mismo libro sobre la recursividad de la cola:

La recursividad de llamada de cola es una técnica que convierte una función recursiva regular en una versión optimizada que puede manejar entradas grandes sin riesgos ni efectos secundarios.

NOTA La razón principal para una llamada de cola como optimización es mejorar la localidad de datos, el uso de memoria y el uso de caché. Al hacer una llamada de cola, la persona que llama utiliza el mismo espacio de pila que la persona que llama. Esto reduce la presión de la memoria. Mejora marginalmente el caché porque la misma memoria se reutiliza para las personas que llaman posteriormente y puede permanecer en el caché, en lugar de desalojar una línea de caché anterior para dejar espacio para una nueva línea de caché.

Vadim S.
fuente