Hay momentos en que usar la recursión es mejor que usar un bucle, y momentos en los que usar un bucle es mejor que usar la recursión. Elegir el "correcto" puede ahorrar recursos y / o generar menos líneas de código.
¿Hay casos en los que una tarea solo se puede hacer usando recursividad, en lugar de un bucle?
INC (r)
,JZDEC (r, z)
puede implementar una máquina de Turing. No tiene 'recursividad', eso es un salto si cero más DECremento. Si la función de Ackermann es computable (lo es), esa máquina de registro puede hacerlo.Respuestas:
Si y no. En última instancia, no hay nada que la recursión pueda calcular que el bucle no pueda, pero el bucle requiere mucha más fontanería. Por lo tanto, lo único que puede hacer la recursión que los bucles no pueden hacer es que algunas tareas sean muy fáciles.
Toma caminando un árbol. Caminar por un árbol con recursión es una estupidez fácil. Es lo más natural del mundo. Caminar un árbol con bucles es mucho menos sencillo. Debe mantener una pila u otra estructura de datos para realizar un seguimiento de lo que ha hecho.
A menudo, la solución recursiva a un problema es más bonita. Ese es un término técnico, y es importante.
fuente
A
que encuentra algo en un árbol. CadaA
vez que encuentra esa cosa, lanza otra función recursivaB
que encuentra una cosa relacionada en el subárbol en la posición donde fue lanzadaA
. Una vez queB
finaliza la recursión, regresaA
y la última continúa su propia recursión. Uno puede declarar una pila paraA
y una paraB
, o poner laB
pila dentro delA
bucle. Si uno insiste en usar una sola pila, las cosas se ponen realmente complicadas.Therefore, the one thing recursion can do that loops can't is make some tasks super easy.
Y lo único que pueden hacer los bucles que la recursividad no puede hacer es que algunas tareas sean súper fáciles. ¿Has visto las cosas feas e poco intuitivas que tienes que hacer para transformar la mayoría de los problemas naturalmente iterativos de una recursión ingenua a una recursiva de cola para que no exploten?map
ofold
(de hecho, si elige considerarlas primitivas, creo que puede usarfold
/unfold
como una tercera alternativa a los bucles o recursividad). A menos que esté escribiendo código de biblioteca, no hay tantos casos en los que deba preocuparse por la implementación de la iteración, en lugar de la tarea que se supone que debe realizar; en la práctica, eso significa que los bucles explícitos y la recursividad explícita son igualmente pobres abstracciones que deben evitarse en el nivel superior.No.
Manos a los mismos fundamentos de los mínimos necesarios con el fin de calcular, sólo tiene que ser capaz de bucle (esto por sí solo no es suficiente, sino que es un componente necesario). No importa cómo .
Cualquier lenguaje de programación que pueda implementar una máquina de Turing se llama Turing completo . Y hay muchos idiomas que se están completando.
¿Mi idioma favorito en el camino de "eso realmente funciona"? La integridad de Turing es la de FRACTRAN , que es completa de Turing . Tiene una estructura de bucle y puede implementar una máquina de Turing en ella. Por lo tanto, cualquier cosa que sea computable puede implementarse en un lenguaje que no tenga recursividad. Por lo tanto, no hay nada que la recursividad pueda brindarle en términos de computabilidad que un bucle simple no pueda.
Esto realmente se reduce a unos pocos puntos:
Esto no quiere decir que hay algunas clases de problemas que pueden pensarse más fácilmente con recursión en lugar de con bucle, o con bucle en lugar de con recursión. Sin embargo, estas herramientas también son igualmente poderosas.
Y aunque llevé esto al extremo de 'esolang' (principalmente porque puedes encontrar cosas que están completadas e implementadas de manera bastante extraña), esto no significa que los esolangs sean de ninguna manera opcionales. Hay una lista completa de cosas que se completan accidentalmente, incluidas las plantillas Magic the Gathering, Sendmail, MediaWiki y el sistema de tipo Scala. Muchos de estos están lejos de ser óptimos cuando se trata de hacer algo práctico, es solo que puedes calcular cualquier cosa que sea computable usando estas herramientas.
Esta equivalencia puede ser particularmente interesante cuando ingresas en un tipo particular de recursión conocido como llamada de cola .
Si tiene, digamos, un método factorial escrito como:
Este tipo de recursión se reescribirá como un bucle; no se utilizará ninguna pila. Dichos enfoques suelen ser más elegantes y fáciles de entender que el bucle equivalente que se está escribiendo, pero nuevamente, para cada llamada recursiva puede haber un bucle equivalente escrito y para cada bucle puede haber una llamada recursiva escrita.
También hay momentos en los que la conversión del bucle simple en una llamada recursiva puede ser complicada y más difícil de entender.
Si desea entrar en el lado de la teoría, vea la tesis de Church Turing . También puede encontrar útil la tesis de la iglesia-turing en CS.SE.
fuente
Siempre puede convertir el algoritmo recursivo en un bucle, que utiliza una estructura de datos de último en entrar, primero en salir (pila AKA) para almacenar el estado temporal, porque la llamada recursiva es exactamente eso, almacenar el estado actual en una pila, proceder con el algoritmo, luego restaurando el estado. Entonces la respuesta corta es: No, no hay tales casos .
Sin embargo, se puede hacer un argumento para "sí". Tomemos un ejemplo concreto y fácil: ordenar por fusión. Debe dividir los datos en dos partes, combinar las partes y luego combinarlas. Incluso si no realiza una llamada a la función del lenguaje de programación real para fusionar la ordenación con el fin de fusionar la ordenación en las partes, debe implementar una funcionalidad que sea idéntica a hacer una llamada a la función (empuje el estado a su propia pila, salte a inicio del ciclo con diferentes parámetros de inicio, luego saque el estado de su pila).
¿Es recursiva, si implementa la recursividad, llame usted mismo, como pasos separados de "estado de empuje" y "salto al principio" y "estado emergente"? Y la respuesta a eso es: No, todavía no se llama recursividad, se llama iteración con pila explícita (si desea utilizar la terminología establecida).
Tenga en cuenta que esto también depende de la definición de "tarea". Si la tarea es ordenar, puede hacerlo con muchos algoritmos, muchos de los cuales no necesitan ningún tipo de recursión. Si la tarea es implementar un algoritmo específico, como ordenar por fusión, entonces se aplica la ambigüedad anterior.
Entonces, consideremos la pregunta, ¿hay tareas generales, para las cuales solo hay algoritmos de recursión? Del comentario de @WizardOfMenlo bajo la pregunta, la función de Ackermann es un ejemplo simple de eso. Por lo tanto, el concepto de recursión es independiente, incluso si se puede implementar con una construcción de programa de computadora diferente (iteración con pila explícita).
fuente
Depende de cuán estrictamente defina "recursividad".
Si estrictamente requerimos que involucre la pila de llamadas (o cualquier mecanismo para mantener el estado del programa que se use), entonces siempre podemos reemplazarlo por algo que no lo haga. De hecho, los lenguajes que conducen naturalmente al uso intensivo de la recursividad tienden a tener compiladores que hacen un uso intensivo de la optimización de las llamadas de cola, por lo que lo que escribe es recursivo pero lo que ejecuta es iterativo.
Pero consideremos un caso en el que hacemos una llamada recursiva y usamos el resultado de una llamada recursiva para esa llamada recursiva.
Hacer que la primera llamada recursiva sea iterativa es fácil:
Luego podemos limpiar y quitar el
goto
para alejar a los velociraptores y la sombra de Dijkstra:Pero para eliminar las otras llamadas recursivas tendremos que almacenar los valores de algunas llamadas en una pila:
Ahora, cuando consideramos el código fuente, ciertamente hemos convertido nuestro método recursivo en uno iterativo.
Teniendo en cuenta para qué se ha compilado, hemos convertido el código que usa la pila de llamadas para implementar la recursión en un código que no lo hace (y al hacerlo, el código convertido que arrojará una excepción de desbordamiento de la pila incluso para valores bastante pequeños en el código que simplemente tomar un tiempo insoportablemente largo para regresar [consulte ¿Cómo puedo evitar que mi función Ackerman desborde la pila? para obtener más optimizaciones que hacen que realmente regrese para muchas más entradas posibles]).
Teniendo en cuenta cómo se implementa generalmente la recursividad, hemos convertido el código que usa la pila de llamadas en código que usa una pila diferente para contener las operaciones pendientes. Por lo tanto, podríamos argumentar que todavía es recursivo, cuando se considera en ese nivel bajo.
Y a ese nivel, de hecho no hay otras formas de evitarlo. Entonces, si considera que ese método es recursivo, entonces hay cosas que no podemos hacer sin él. Generalmente, aunque no etiquetamos dicho código de forma recursiva. El término recursividad es útil porque cubre un cierto conjunto de enfoques y nos da una manera de hablar sobre ellos, y ya no estamos usando uno de ellos.
Por supuesto, todo esto supone que tienes una opción. Hay dos idiomas que prohíben las llamadas recursivas, y los idiomas que carecen de las estructuras de bucle necesarias para iterar.
fuente
La respuesta clásica es "no", pero permítanme explicar por qué creo que "sí" es una mejor respuesta.
Antes de continuar, eliminemos algo: desde el punto de vista de la computabilidad y la complejidad:
Bien, ahora, pongamos un pie en tierra de práctica, manteniendo el otro pie en tierra de teoría.
La pila de llamadas es una estructura de control , mientras que una pila manual es una estructura de datos . El control y los datos no son conceptos iguales, pero son equivalentes en el sentido de que pueden reducirse entre sí (o "emularse" entre sí) desde el punto de vista de la computabilidad o la complejidad.
¿Cuándo podría importar esta distinción? Cuando trabajas con herramientas del mundo real. Aquí hay un ejemplo:
Digamos que estás implementando N-way
mergesort
. Es posible que tenga unfor
bucle que atraviesa cada uno de losN
segmentos, los llamamergesort
por separado y luego combina los resultados.¿Cómo podría paralelizar esto con OpenMP?
En el ámbito recursivo, es muy sencillo: sólo hay que poner
#pragma omp parallel for
alrededor de su bucle que va de 1 a N, y ya está. En el reino iterativo, no puedes hacer esto. Debe generar hilos manualmente y pasarles los datos apropiados manualmente para que sepan qué hacer.Por otro lado, hay otras herramientas (como los vectorizadores automáticos, por ejemplo
#pragma vector
) que funcionan con bucles pero son completamente inútiles con la recursividad.El punto es que solo porque puede probar que los dos paradigmas son matemáticamente equivalentes, eso no significa que sean iguales en la práctica. Un problema que podría ser trivial para automatizar en un paradigma (por ejemplo, la paralelización de bucles) podría ser mucho más difícil de resolver en el otro paradigma.
es decir: las herramientas para un paradigma no se traducen automáticamente a otros paradigmas.
En consecuencia, si necesita una herramienta para resolver un problema, es probable que la herramienta solo funcione con un tipo particular de enfoque y, en consecuencia, no podrá resolver el problema con un enfoque diferente, incluso si puede demostrar matemáticamente que el problema puede ser resuelto de cualquier manera.
fuente
Dejando a un lado el razonamiento teórico, echemos un vistazo a cómo se ven la recursividad y los bucles desde el punto de vista de la máquina (hardware o virtual). La recursión es una combinación de flujo de control que permite iniciar la ejecución de algún código y regresar al finalizar (en una vista simplista cuando se ignoran las señales y excepciones) y de los datos que se pasan a ese otro código (argumentos) y que se devuelven de es (resultado) Por lo general, no se trata de una gestión explícita de la memoria; sin embargo, existe una asignación implícita de la memoria de la pila para guardar direcciones de retorno, argumentos, resultados y datos locales intermedios.
Un bucle es una combinación de flujo de control y datos locales. Comparando esto con la recursión, podemos ver que la cantidad de datos en este caso es fija. La única forma de romper esta limitación es usar memoria dinámica (también conocida como montón ) que se puede asignar (y liberar) cuando sea necesario.
Para resumir:
Suponiendo que la parte de flujo de control es razonablemente potente, la única diferencia está en los tipos de memoria disponibles. Entonces, nos quedan 4 casos (el poder de expresividad aparece entre paréntesis):
Si las reglas del juego son un poco más estrictas y no se permite la implementación recursiva para usar bucles, obtenemos esto en su lugar:
La diferencia clave con el escenario anterior es que la falta de memoria de pila no permite la recursividad sin bucles para realizar más pasos durante la ejecución que las líneas de código.
fuente
Si. Hay varias tareas comunes que son fáciles de realizar utilizando la recursión pero imposibles con solo bucles:
fuente
Hay una diferencia entre las funciones recursivas y las funciones recursivas primitivas. Las funciones recursivas primitivas son aquellas que se calculan utilizando bucles, donde el recuento máximo de iteraciones de cada bucle se calcula antes de que comience la ejecución del bucle. (Y "recursivo" aquí no tiene nada que ver con el uso de la recursividad).
Las funciones recursivas primitivas son estrictamente menos potentes que las funciones recursivas. Obtendría el mismo resultado si tomara funciones que utilizan la recursividad, donde la profundidad máxima de la recursión debe calcularse de antemano.
fuente
Si está programando en c ++ y usa c ++ 11, entonces hay una cosa que debe hacerse usando recursiones: las funciones constexpr. Pero el estándar limita esto a 512, como se explica en esta respuesta . El uso de bucles en este caso no es posible, ya que en ese caso la función no puede ser constexpr, pero esto se cambia en c ++ 14.
fuente
fuente
Estoy de acuerdo con las otras preguntas. No hay nada que pueda hacer con la recursividad que no pueda hacer con un bucle.
PERO , en mi opinión, la recursión puede ser muy peligrosa. Primero, para algunos es más difícil entender lo que realmente está sucediendo en el código. En segundo lugar, al menos para C ++ (Java, no estoy seguro), cada paso de recursión tiene un impacto en la memoria porque cada llamada al método provoca la acumulación de memoria y la inicialización del encabezado de los métodos. De esta manera puedes volar tu pila. Simplemente intente la recursividad de los números de Fibonacci con un valor de entrada alto.
fuente