¿Se pueden modelar todos los algoritmos iterativos de forma recursiva y viceversa?

Es cierto que todos los algoritmos iterativos se pueden describir de forma recursiva. De hecho, los programadores funcionales definen la iteración como el caso especial de recursión donde todas las llamadas recursivas son recursivas de cola . La traducción es puramente mecánica: simplemente convierta todos los estados variables en argumentos de función. Por ejemplo,

def foo(): s = 0 i = 0 while i < 10: s = s + i i = i + 1 return s print foo() 

puede reescribirse como

 def foo(s, i): if i < 10: return foo(s + i, i + 1) return s print foo(0, 0) 

¿Podemos revertir esta transformación para convertir un algoritmo recursivo en uno iterativo? Bueno, eso depende de lo que llames “iterativo”.

Dada una función recursiva general:

 def fib(n): if n <= 1: return n return fib(n - 1) + fib(n - 2) print fib(10) 

siempre podemos convertirlo en una función recursiva de cola reescribiéndolo en un estilo de paso de continuación :

 def fibc(n, cont): if n <= 1: return cont(n) return fibc(n - 1, lambda a: fibc(n - 2, lambda b: cont(a + b))) print fibc(10, lambda x: x) 

y ahora podemos llevar todas las llamadas recursivas de cola a un bucle de evaluación externo:

 def fibi(n, cont): if n <= 1: return (cont, [n]) return (fibi, [n - 1, lambda a: (fibi, [n - 2, lambda b: (cont, [a + b])])]) (f, args) = (fibi, [10, None]) while f is not None: (f, args) = f(*args) [ret] = args print ret 

Esencialmente, hemos reificado la pila de recursión como una estructura de datos explícita (aquí una que está hecha de cierres lambda , pero podríamos fácilmente salir de estructuras más tradicionales como listas). El código resultante ciertamente usa un ciclo iterativo, y podría describirse como “no recursivo” en el sentido de que nunca usa más de un nivel de la pila del lenguaje. Pero es difícil argumentar que esta transformación mecánica cuenta como algo más que llevar la recurrencia a un nivel diferente de interpretación.

Si comienza con un algoritmo recursivo que no es recursivo de la cola, puede ser necesario reescribirlo como un algoritmo “verdaderamente iterativo”, si es que es posible.

 (a, b) = (0, 1) for i in xrange(10): (a, b) = (b, a + b) print a 

Esta respuesta es básicamente una toma de nivel inferior de la respuesta de Anders.

Tengo que estar (algo) en desacuerdo con la respuesta de Isaac.
Creo que se puede decir que el ensamblaje (bien estructurado) presenta recursividad, aunque todo es más loco en el ensamblaje.

De hecho, creo que el código de ensamblaje será algo útil para 1 / probar el punto que acabo de hacer 2 / explicar el “enlace” entre el código iterativo y el código recursivo, lo que me permitirá probar que 3 / cualquier algoritmo puede implementarse usando cualquier paradigma.

Esta es una implementación del algoritmo factorial recursivo ingenuo en el ensamblaje nasm (lo siento, es bastante largo, y lo siento si no es perfecto, mi ensamblaje está un poco oxidado y tengo que hacer que este código sea lo más simple posible de entender):

  scanf externo
 impresión externa

 principal global
 principal:
         empuje 0;  El valor scanf se modificará para obtener nuestro número                     
         empujar esp;  La dirección de ese valor                                         
         empujar scanfstring;  La cadena "% d"                                                   
         llamada scanf
         pop eax;  Aquí el resultado está convenientemente a la derecha                      
         pop eax;  colocar en la pila, no es necesario reventarlo                             

         llamada factorial;  Llamando factorial con nuestro argumento                               

         empujar eax
         push printfstring
         llamar a printf;  Imprimir el resultado                                               
         pop eax
         pop eax

         mov eax, 1;  Saliendo (solo funciona en Linux)                                     
         int 80h

 factorial:
         mov ecx, [esp + 4];  Obteniendo el argumento                                              

         prueba ecx, ecx;  Comprobando por 0                                                    
         jz on_zero

         sub ecx, 1
         empujar ecx
         llamada factorial;  Llamada recursiva con argumento-1                                    
         pop ecx

         mov ecx, [esp + 4];  Calculando el resultado                                              
         imul eax, ecx
         jubilado
 en cero:
         mov eax, 1;  Devuelve 1 si el argumento es 0                                         
         jubilado

 sección .rodata
 scanfstring: db '% d', 0
 printfstring: db '% d', 10,0 

En pseudocódigo, esto lee

  factorial (valor)
   valor de retorno * factorial (valor - 1) 

Primero, la función factorial es de hecho recursiva, y no recursiva de cola, completamente recursiva.

Segundo, este código permite ver el enlace (y por lo tanto la “equivalencia”) entre el código iterativo y el código recursivo: la pila.
De hecho, creo que sería justo decir que el ensamblaje no admite totalmente el paradigma iterativo (ya que a menudo tiene pocas construcciones de bucle real) ni el paradigma recursivo (ya que uno tiene que manipular la pila uno mismo), y esto permite exponer lo que el Las construcciones están hechas de.

La parte más importante del código de ensamblaje anterior es la llamada recursiva a factorial. Se puede ver que consiste en empujar un valor a una pila implícita, llamar factorial y luego extraer el valor de la pila. Esta pila implícita es lo único que permite que algunos algoritmos sean más fáciles de implementar de forma recursiva que iterativa.
Sin embargo, uno siempre puede implementar un algoritmo recursivo iterativamente usando una pila explícita.

Aquí está el equivalente iterativo C exacto de este algoritmo, usando una pila explícita (muy básica):

 #include  int main() { int stack[1000]; // This can overflow, but it's just an example int *stack_ptr = stack; int current_value; scanf("%d", &current_value); while (current_value > 0) { *stack_ptr = current_value; current_value--; stack_ptr++; } int result = 1; while (stack_ptr != stack) { stack_ptr--; result *= *stack_ptr; } printf("%d\n", result); } 

Ahora, hasta donde sé, básicamente nunca vale la pena usar una pila explícita en lugar de una función recursiva (aunque en realidad he visto esta técnica utilizada en la vida real, estaba en el medio de una función ya gigantesca y el programador aparentemente se opuso a dividir su código en varias funciones …).

Además, este es un ejemplo simple, pero si una función es recursiva en varios lugares, la pila explícita tendrá que ser más compleja ya que el segundo ciclo while tendrá que dividirse en varias partes (usando ifs o una instrucción switch) y stack tendrá que especificar qué parte debe procesar cada pieza de datos.

Básicamente, una función recursiva de cola es una función recursiva que no usará su pila para nada útil, por lo que la pila puede dejarse sin usar para acelerar ligeramente la función y evitar desbordamientos de stock.

More Interesting

¿Cómo se llega a una estructura de datos totalmente nueva?

¿Cuál es el papel de las matemáticas en las computadoras?

¿Cuál es la razón por la cual las instalaciones no cambian su esquema de cifrado, de modo que cuando se publique una prueba de P = NP no se verán afectados?

Si desmantelo un cubo de Rubik y luego lo vuelvo a montar de todas las formas posibles, ¿cuántos cubos distintos de Rubik son posibles?

¿Alguien podría recomendar algunos temas interesantes para dominar que se encuentran en la intersección de la informática / programación y la teoría / lógica de conjuntos (algo práctico, no solo teórico)?

Siendo un estudiante de matemáticas BSc sin cursos de computación, ¿cómo puedo aprender codificación para ser competitivo?

¿Son defectuosos los números complejos?

¿Cómo los operadores matemáticos mapean objetos de un punto a otro en el espacio?

¿Cuál es el espacio nulo de un operador?

Una fábrica produce bombillas defectuosas con cierta probabilidad, p. Se sabe que p es pequeño: alrededor del 1%, pero se desconoce el valor exacto. ¿Cuál es el tamaño de muestra que tomaría para estimar el valor de p?

¿Cuál es la complejidad temporal de un programa que calcula el número n de Fibonacci mediante la memorización?

¿Por qué soy bueno en cursos intensivos de programación, pero sigo reprobando en cursos de teoría de informática? ¿Estoy en condiciones de ser ingeniero de software?

¿Puede una inteligencia computacional observar una ley física incontestable?

¿Se puede aplicar la proporción áurea (Fibonacci) con beneficios en informática?

¿Se consideran [matemáticas] \ matemáticas O (n ^ {\ log n}) [/ matemáticas] y [matemáticas] \ matemáticas O (n ^ {1+ \ log n}) [/ matemáticas] las mismas clases de complejidad?