He usado bastante la recursividad en mis muchos años de programación para resolver problemas simples, pero soy plenamente consciente de que a veces necesitas iteración debido a problemas de memoria / velocidad.
Entonces, en algún momento en el pasado, fui a tratar de encontrar si existía algún "patrón" o forma de libro de texto de transformar un enfoque de recursión común a la iteración y no encontré nada. O al menos nada que pueda recordar sería útil.
- ¿Hay reglas generales?
- ¿Hay un "patrón"?
recursion
computer-science
theory
iteration
Gustavo Carreño
fuente
fuente
Respuestas:
Por lo general, reemplazo un algoritmo recursivo por un algoritmo iterativo empujando los parámetros que normalmente se pasarían a la función recursiva en una pila. De hecho, está reemplazando la pila de programas por una propia.
Nota: si tiene más de una llamada recursiva dentro y desea preservar el orden de las llamadas, debe agregarlas en el orden inverso a la pila:
tiene que ser reemplazado por
Editar: El artículo Pilas y Eliminación de recursividad (o enlace de Copia de seguridad del artículo ) entra en más detalles sobre este tema.
fuente
(node)->()
con(node)->[actions]
donde está la acción() -> [actions]
. Luego, en el exterior, simplemente saca una acción / continuación de la pila, la aplica / ejecuta, empuja las acciones que devolvió en la pila en orden inverso y repite. Contingente / recorridos complejos, que acaba de capturar lo que habría sido variables de pila locales en los punteros de referencia contado que cierre otra vez en sus procesadores, entonces procesadores posteriores pueden estar supeditada a los resultados de sub-recorridos anteriores, etc.new
podemos crear un objeto en el montón en lugar de la pila. A diferencia de la pila, el montón no tiene restricciones de memoria. Ver gribblelab.org/CBootCamp/7_Memory_Stack_vs_Heap.htmlRealmente, la forma más común de hacerlo es mantener su propia pila. Aquí hay una función recursiva de clasificación rápida en C:
Así es como podríamos hacerlo iterativo manteniendo nuestra propia pila:
Obviamente, este ejemplo no verifica los límites de la pila ... y realmente podría dimensionar la pila en función del peor de los casos dados los valores izquierdo y derecho. Pero se entiende la idea.
fuente
O(N) = O(R*L)
, dondeL
es la suma de la complejidad "para la capa r", por ejemplo, en este caso tieneO(N)
trabajo en cada paso haciendo las particiones, la profundidad recursiva esO(R)
, es decir, en el peor de los casosO(N)
, el caso promedioO(logN)
aquí.Parece que nadie ha abordado dónde la función recursiva se llama a sí misma más de una vez en el cuerpo, y maneja el regreso a un punto específico en la recursión (es decir, no primitivo-recursivo). Se dice que cada recursión puede convertirse en iteración , por lo que parece que esto debería ser posible.
Se me ocurrió un ejemplo de C # de cómo hacer esto. Suponga que tiene la siguiente función recursiva, que actúa como un recorrido de postorder, y que AbcTreeNode es un árbol de 3 arios con punteros a, b, c.
La solución iterativa:
fuente
Esfuércese por hacer su llamada recursiva Tail Recursion (recursividad donde la última declaración es la llamada recursiva). Una vez que tenga eso, convertirlo a iteración es generalmente bastante fácil.
fuente
Bueno, en general, la recursión se puede imitar como iteración simplemente usando una variable de almacenamiento. Tenga en cuenta que la recursividad y la iteración son generalmente equivalentes; uno casi siempre se puede convertir al otro. Una función recursiva de cola se convierte muy fácilmente en una iterativa. Simplemente haga que la variable del acumulador sea local e itere en lugar de recurse. Aquí hay un ejemplo en C ++ (C si no fuera por el uso de un argumento predeterminado):
Conociéndome, probablemente cometí un error en el código, pero la idea está ahí.
fuente
Incluso usar stack no convertirá un algoritmo recursivo en iterativo. La recursión normal es una recursión basada en funciones y si usamos stack se convierte en una recursión basada en stack. Pero sigue siendo una recursión.
Para algoritmos recursivos, la complejidad del espacio es O (N) y la complejidad del tiempo es O (N). Para algoritmos iterativos, la complejidad del espacio es O (1) y la complejidad del tiempo es O (N).
Pero si usamos cosas de pila en términos de complejidad, permanece igual. Creo que solo la recursión de la cola se puede convertir en iteración.
fuente
copy = new int[size]; for(int i=0; i<size; ++i) copy[i] = source[i];
espacio de la memoria y la complejidad del tiempo son ambas O (N) en función del tamaño de los datos, pero es claramente un algoritmo iterativo.El artículo de eliminación de pilas y recursividad captura la idea de externalizar el marco de la pila en el montón, pero no proporciona una forma directa y repetible de convertir. Debajo hay uno.
Al convertir a código iterativo, uno debe ser consciente de que la llamada recursiva puede ocurrir desde un bloque de código arbitrariamente profundo. No son solo los parámetros, sino también el punto para volver a la lógica que queda por ejecutar y el estado de las variables que participan en condicionales posteriores, lo que importa. A continuación se muestra una forma muy simple de convertir a código iterativo con los menores cambios.
Considere este código recursivo:
Código iterativo:
Observe cómo la estructura del código sigue siendo fiel a la lógica recursiva y las modificaciones son mínimas, lo que resulta en una menor cantidad de errores. A modo de comparación, he marcado los cambios con ++ y -. La mayoría de los nuevos bloques insertados, excepto v.push_back, son comunes a cualquier lógica iterativa convertida
+++++++++++++++++++++++++
------------------------
+++++++++++++++++++++++++
-------------------------
+++++++++++++++++++++++++
-------------------------
+++++++++++++++++++++++++
-------------------------
fuente
stackitem
objetos se asignan con un valor de basura parara
. Todo sigue funcionando en el caso más parecido, perora
por coincidencia debe ser 1 o 2, obtendrá un comportamiento incorrecto. La solución es inicializarra
a 0.stackitem
no se debe presionar sin inicializar. Pero sí, inicializar a 0 detectaría errores.v.pop_back()
declaración?Busque en Google "Estilo de paso continuo". Hay un procedimiento general para convertir a un estilo recursivo de cola; También hay un procedimiento general para convertir las funciones recursivas de cola en bucles.
fuente
Simplemente matando el tiempo ... Una función recursiva
se puede convertir a
fuente
En general, la técnica para evitar el desbordamiento de la pila para las funciones recursivas se llama técnica de trampolín, que es ampliamente adoptada por los desarrolladores de Java.
Sin embargo, para C # hay un pequeño método auxiliar aquí que convierte su función recursiva en iterativa sin necesidad de cambiar la lógica o hacer que el código sea incomprensible. C # es un lenguaje tan agradable que es posible hacer cosas increíbles con él.
Funciona envolviendo partes del método mediante un método auxiliar. Por ejemplo, la siguiente función recursiva:
Se convierte en:
fuente
Pensando en cosas que realmente necesitan una pila:
Si consideramos el patrón de recursión como:
Por ejemplo, la clásica Torre de Hanoi.
Esto se puede traducir en un bucle que funciona en una pila explícita, reformulándolo como:
Para la Torre de Hanoi esto se convierte en:
Aquí hay una flexibilidad considerable en cuanto a cómo define su pila. Puede hacer que su pila sea una lista de
Command
objetos que hacen cosas sofisticadas. O puede ir en la dirección opuesta y hacer que sea una lista de tipos más simples (por ejemplo, una "tarea" podría ser 4 elementos en una pila deint
, en lugar de un elemento en una pila deTask
).Todo esto significa que la memoria para la pila está en el montón en lugar de en la pila de ejecución de Java, pero esto puede ser útil ya que tiene más control sobre ella.
fuente
Un patrón a buscar es una llamada de recursión al final de la función (llamada recursión de cola). Esto se puede reemplazar fácilmente con un tiempo. Por ejemplo, la función foo:
termina con un llamado a foo. Esto se puede reemplazar con:
que elimina la segunda llamada recursiva.
fuente
Una pregunta que se había cerrado como duplicado de esta tenía una estructura de datos muy específica:
El nodo tenía la siguiente estructura:
La función de eliminación recursiva se veía así:
En general, no siempre es posible evitar una pila para funciones recursivas que se invocan más de una vez (o incluso una vez). Sin embargo, para esta estructura particular, es posible. La idea es aplanar todos los nodos en una sola lista. Esto se logra colocando los nodos actuales
child
al final de la lista de la fila superior.Esta técnica se puede aplicar a cualquier estructura vinculada a datos que se pueda reducir a un DAG con un orden topológico determinista. Los nodos actuales se reorganizan para que el último niño adopte a todos los otros niños. Luego, el nodo actual se puede eliminar y el recorrido puede iterar al hijo restante.
fuente
La recursión no es más que el proceso de llamar a una función desde la otra, solo este proceso se realiza llamando a una función por sí misma. Como sabemos cuando una función llama a la otra función, la primera función guarda su estado (sus variables) y luego pasa el control a la función llamada. La función llamada se puede llamar usando el mismo nombre de variables ex fun1 (a) puede llamar a fun2 (a). Cuando hacemos llamadas recursivas no sucede nada nuevo. Una función se llama a sí misma al pasar el mismo tipo y similares en las variables de nombre (pero obviamente los valores almacenados en las variables son diferentes, solo el nombre permanece igual). Pero antes de cada llamada, la función guarda su estado y este proceso de guardar continúa. EL AHORRO SE HACE EN UNA PILA.
AHORA LA PILA ENTRA EN JUEGO.
Entonces, si escribe un programa iterativo y guarda el estado en una pila cada vez y luego saca los valores de la pila cuando sea necesario, ¡ha convertido con éxito un programa recursivo en uno iterativo!
La prueba es simple y analítica.
En la recursividad, la computadora mantiene una pila y en la versión iterativa tendrá que mantener manualmente la pila.
Piénselo bien, simplemente convierta un programa recursivo de primera búsqueda en profundidad (en gráficos) en un programa iterativo dfs.
¡Todo lo mejor!
fuente
Otro ejemplo simple y completo de convertir la función recursiva en iterativa usando la pila.
fuente
Una descripción aproximada de cómo un sistema toma cualquier función recursiva y la ejecuta usando una pila:
Esto pretendía mostrar la idea sin detalles. Considere esta función que imprimiría nodos de un gráfico:
Por ejemplo, gráfico: A-> B A-> C show (A) imprimiría B, A, C
Las llamadas a funciones significan guardar el estado local y el punto de continuación para que pueda regresar y luego saltar la función que desea llamar.
Por ejemplo, suponga que show (A) comienza a ejecutarse. La llamada a la función en la línea 3. show (B) significa: Agregar elemento a la pila que significa "deberá continuar en la línea 2 con el nodo de estado variable local = A" - Ir a la línea 0 con nodo = B.
Para ejecutar el código, el sistema ejecuta las instrucciones. Cuando se encuentra una llamada a la función, el sistema envía la información que necesita para volver a donde estaba, ejecuta el código de la función y, cuando la función se completa, muestra la información sobre dónde debe ir para continuar.
fuente
Este enlace proporciona alguna explicación y propone la idea de mantener la "ubicación" para poder llegar al lugar exacto entre varias llamadas recursivas:
Sin embargo, todos estos ejemplos describen escenarios en los que una llamada recursiva se realiza una cantidad fija de veces. Las cosas se ponen más complicadas cuando tienes algo como:
fuente
Hay una forma general de convertir el recorrido recursivo a iterador utilizando un iterador perezoso que concatena múltiples proveedores de iteradores (expresión lambda que devuelve un iterador). Vea mi Conversión del recorrido recursivo a iterador .
fuente
Mis ejemplos están en Clojure, pero deberían ser bastante fáciles de traducir a cualquier idioma.
Dada esta función que
StackOverflow
es para valores grandes de n:podemos definir una versión que use su propia pila de la siguiente manera:
donde
return
se define como:Esto también funciona para funciones más complejas, por ejemplo, la función ackermann :
se puede transformar en:
fuente