Informática: ¿Por qué la recursión es más elegante que la iteración?

La iteración incorpora demasiados detalles innecesarios en su código, lo que hace que sea más difícil identificar lo que hace el código de un vistazo. A menudo requiere que ejecutes instrucciones mentales antes de que puedas comprender por qué son necesarios pasos individuales. No quiero tener que hacer un seguimiento de las longitudes y los contadores y los estados intermedios cuando podría usar fácilmente “y el resto” o “la suma de todos estos”, para lo cual la recursividad se presta mucho mejor.

Por ejemplo, comparemos una implementación iterativa y recursiva de un producto punto. En aras de la simplicidad, ambas implementaciones supondrán que sus argumentos tienen la misma longitud.
Usaré Python para la implementación iterativa y Haskell para la implementación recursiva.

Iterativo

def dotProduct(a, b):
total = 0
for i in range(len(a)):
total += a[i] * b[i]
return total

Cuando miro esta implementación iterativa, lo que veo es:

  1. Escribe un total de 0.
  2. Determine la longitud de sus vectores y cuente hasta esta longitud: 1, comenzando desde 0.
  3. Con cada recuento i , examine el iésimo valor dentro de cada vector, multiplíquelos y luego agregue este producto a su total .
  4. Cuando haya terminado de contar hasta la longitud del vector – 1, el producto de puntos es el total que ha anotado en este punto.

Este procedimiento no me dice lo que estoy haciendo sino lo que quiere que haga . Es como construir muebles de IKEA: cada paso requiere que hagas algo específico que proporcione pocas o ninguna pista sobre la razón para hacerlo, y hasta que termines de leer todas las instrucciones para el contexto, simplemente debes confiar en que es necesario. Las ideas codificadas en forma iterativa requieren que usted vuelva a derivar el propósito de hacerlo cada vez que las lea.

Recursivo

dotProduct [] [] = 0
dotProduct (a:as) (b:bs) = a * b + dotProduct as bs

Esta implementación recursiva me dice:

  1. El producto escalar de dos vectores vacíos es 0.
  2. El producto escalar de dos vectores no vacíos es el producto de sus valores iniciales más el producto escalar de sus valores restantes.

En esa sola línea, veo toda la idea. No tengo que derivar una explicación de lo que esto hace de una lista de ajustes a las variables de estado; El código ya es una explicación de lo que es un producto punto. Y no necesita hacer uso de detalles extraños como i s o total s o length s, o examinar los valores dentro de cada vector, o cualquier otra distracción para transmitirme eso.

No sirve como una analogía, pero leer código iterativo me recuerda algunas conversaciones desagradables que he tenido con el soporte técnico en las que se negaron a decirme el propósito de las instrucciones que me estaban dando:
Soporte técnico: “Veamos si su computadora se conectó a su enrutador con éxito”.
Yo: “¿Debo ver si tengo una dirección IP?”
Soporte técnico: “Entonces, haga clic en Inicio”.
Yo: “Está bien”.
Soporte técnico: “Ahora haga clic en Ejecutar”.
Yo: “Está bien. ¿Estás tratando de obtener un símbolo del sistema?”
Soporte técnico: “¿Ves un cuadro gris?”
Yo: “Sí. ¿Debo abrir un símbolo del sistema?”
Soporte técnico: “Ahora escriba: C como en Charlie, M como en Mike, D como en Delta”.
Yo: “Está bien, recibí un símbolo del sistema”.
Soporte técnico: “Ahora presione Entrar. Debería ver un cuadro negro”.
Yo: “¿Qué quieres que haga en el aviso?”
Soporte técnico: “Escriba: I como en India, P como en Peter, C como en Charlie …”
Yo: “ipconfig? ¿Quieres saber mi IP?”
Soporte técnico: “… O como en Oscar, N como en noviembre, F como en Foxtrot …”
Yo: “La dirección IPv4 en mi tarjeta inalámbrica es 192.168.1.101. También tengo una dirección IPv6, si la necesita”.
Soporte técnico: “… I como en India, y G como en Golf. Y presiona Enter”.
Yo: “…”
Soporte técnico: “Ahora busque en el texto algo que diga ‘dirección IP'”.
Yo: “Lo encontré. El IPv4 es 192.168.1.101”.
Soporte técnico: “¿Puedes encontrar el que tiene cuatro números separados por puntos?”
Yo: “¿La dirección IPv4? Sí, es 192.168.1.101”.
Soporte técnico: “Está bien, gracias”.
…y así.
(Sí, en realidad he tenido esta conversación).

La recursividad es más general.

Una solución iterativa está bien para datos que son lineales como matrices y listas enlazadas; o procesos que son lineales, como “haz esto 10 veces”.

Pero la iteración se vuelve cada vez más complicada cuando desea atravesar árboles u otras estructuras complejas de profundidades indefinidas. O desea hacer cosas que pueden ramificarse entre diferentes opciones en diferentes casos. O trampolín entre dos procesos complementarios.

A medida que aumenta la complejidad de sus datos / procesos, los enfoques recursivos tienden a realizar un seguimiento, escalando más o menos linealmente con el problema. Los enfoques iterativos pueden ir en espiral hacia sus propias complejidades combinatorias a medida que intentas escribir código adicional para aplanar los datos / tareas complejas en secuencias unidimensionales para que se puedan repetir.

Repetir es humano, recurrir divino.

Realmente se reduce a menos líneas de código y elegancia.

1)

int fact(int n)
{
return (n==1||n==0)?1:n*fact(n-1);
}

2)

hecho de hecho (int n)
{
int i, f = 1;
para (i = 1; i <= n; i ++)
f = f * i;
volver f;
}

Siempre elegiría 1 sobre 2.

La recursión no es fundamentalmente más elegante que la iteración, aunque puede ser más poderosa.

Los programadores funcionales tienden a preferir la recursividad porque la iteración suele ser una técnica imprescindible. Incluso si no considera que el contador de iteraciones en sí sea un estado modificado de forma imperativa, casi siempre hay algún estado que lleva entre iteraciones en el bucle.

La recursividad, por otro lado, le permite calcular el resultado del cómputo completo a partir de los resultados de subcomputaciones, lo cual es bueno para la programación funcional. También se generaliza a estructuras como los árboles, aunque la iteración a veces es más adecuada para conjuntos.

He hecho bastante programación funcional. A diferencia de la mayoría de los otros que respondieron a esta pregunta, me he encontrado con muchos casos en los que la iteración parece una solución más elegante para un problema que la recursividad. Pero a menudo no está disponible o al menos se desalienta debido a construcciones de lenguaje (te estoy mirando, Haskell), lo que puede resultar en un código mucho menos legible. Esto ocurre con mayor frecuencia cuando un algoritmo necesita rastrear muchos acumuladores, pero cada uno no se actualiza en la mayoría de las iteraciones.

No estoy de acuerdo con que se trate de la longitud del código. En mi opinión, es el hecho de que generalmente hay menos conceptos con los que lidiar.

Las soluciones recursivas tienen el mismo aspecto:

  • Reduce el problema a una forma más simple.
  • Identifique formas del problema que sean demasiado simples para reducirse.
  • Resuelve las formas más simples del problema.
  • Recopile y combine los resultados del componente.

Cuatro piezas (o dos piezas cada una con dos piezas, si lo prefiere) es una idea convincente, especialmente cuando esas partes son solo cosas que puede enchufar. No hay forma de descomponer una solución iterativa, simplemente … haga cosas … hasta que Ya terminaste.

Si le gusta la teoría de la complejidad, debería ser trivial escribir la relación de recurrencia (ya que cada pieza tiene una complejidad y la relación entre las partes es consistente), lo que significa que encontrar la complejidad general es solo matemática.

Menos código también es bueno, pero no es lo que yo llamaría elegante … o necesariamente cierto en todos los casos, especialmente si no puede aprovechar la pila del sistema.

La iteración es detallada, la recursividad es mucho más limpia.
Ambas técnicas logran el mismo objetivo, la llamada recursiva no le ahorra un espacio de almacenamiento, ya que en algún lugar debe mantenerse una pila de los valores que se procesan. Tampoco la recursión funcionará más rápido. La recursión es una iteración de aspecto sexy y mucho más legible, ya que al menos te ahorra una docena de líneas de código. Es especialmente conveniente para estructuras definidas recursivamente como, árboles y algoritmos de búsqueda, calculando los números de Fibonacci.

Al escribir una función recursiva, siempre debe buscar los puntos de ruptura, los casos de esquina de la llamada, de lo contrario, terminará en un bucle infinito que causará una falla de segmentación en algún momento. Primero capture los puntos de ruptura en alguna cláusula if / else y luego realice la llamada recursiva.
Hay un huevo de Pascua de Google Engineers … abre una pestaña de Chrome y busca la recursividad, ¿lo tienes? 😉

Hablando de estilos, la comprensión se usa ampliamente en Python.

Ambos son análogos. Muchas personas prefieren usar la recursión en lugar de la iteración, incluido yo. La razón detrás de esto es cómo miro el problema. Tomemos como ejemplo el recorrido en orden del árbol, cada hoja en un árbol es un árbol en sí mismo, por lo que puedo reducir mi árbol usando la recursión y la resultante será solo árbol.
Es fácil usar la recursividad en lenguaje de programación orientado a objetos, ¿cómo?
Una vez que tenga un objeto en su memoria, puede jugarlo como quiera.
Al leer el archivo xml, cada etiqueta en xml es xml en sí misma, por lo que podemos reducir xml usando recursividad.
La parte más importante es cómo se ve el problema. Si puede reducir el objeto a uno de su propio tipo, entonces recurra a la recursividad. Por cierto, cada algoritmo de recursión puede transformarse en uno con iteración.

Como dijo Andrew Bransford Brown, (a menudo) hay menos líneas de código.

Hay una diferencia entre elegante e inteligente, y algunos enfoques pueden ser inteligentes, pero no realmente elegantes. Por ejemplo, la recursividad no es más elegante si hace que la aplicación explote porque se repite demasiado.

La clave es que el enfoque que elija se ajuste a un enfoque natural, comprensible, funcional, eficiente y sostenible para resolver el problema.

La recursión es más poderosa pero tiene caídas propias. Esp, tiene una alta complejidad espacial en comparación con la iteración. También es más lento que la iteración. Sin embargo, hay ciertos problemas que no pueden abordarse mediante la iteración, por ejemplo. Encontrar todas las permutaciones posibles de una cadena de caracteres N. Aquí es más fácil hacer que el código sea más simple y limpio utilizando el enfoque recursivo en lugar del enfoque iterativo. Aquí hay una pequeña demostración donde he tratado de explicar algunos enfoques recursivos para resolver ciertos problemas:

Javascript – Introducción a la recursión – Golibrary.co

Cuando puede dividir un problema grande en problemas cada vez más pequeños hasta que puedan resolverse trivialmente, comienza a notar que esta es realmente la forma humana de resolver problemas.
Los humanos son, por defecto, más elegantes que las máquinas.

No lo son, son requisitos para diferentes tareas. Las tareas que necesitan recursividad generalmente no se pueden hacer con iteración lineal, y las tareas que se pueden manejar con iteración no se deben manejar con recursión.

Cuando la estructura de datos tiene forma de árbol, lo que significa que se puede estirar infinitamente en cualquier dirección y continuamente tener subnodos que pueden ramificarse de cualquier manera: use la recursividad.

Cuando la estructura de datos tiene 1, 2 o 3 dimensiones establecidas y predefinidas, use la iteración.

En mi opinión, la recursividad es más elegante porque parece más declarativa. Es decir, usted establece su algoritmo en términos del resultado que desea en lugar de cómo desea obtener ese resultado.

Personalmente, reflexiono un poco más sobre un problema si estoy tomando el camino de la recursividad. Entonces lo entiendo un poco mejor.

Creo que lo que lo hace más elegante es que generalmente quieres tomar el camino funcional; lo cual es elegante porque tienes menos variables y son inmutables.

Creo que la elegancia (para la recursión misma) viene en el estado mental al que se presta. Dado que tiene que pensar en la solución “hacia atrás y hacia adelante”. Mientras que escribir un bucle se puede hacer casi sin pensar.

Creo que una gran parte de la respuesta es que, en una recursión, generalmente tienes muchas menos declaraciones con efectos secundarios. Todas las variables tienen un valor fijo.

More Interesting

¿Cuál es la mejor fuente para aprender del algoritmo y la estructura de datos para principiantes?

¿Cuál es el mejor algoritmo para encontrar la ruta más corta en un gráfico orientado, donde algunos bordes están bloqueados y las teclas están en algún lugar de los nodos?

¿Cuáles son los algoritmos de correspondencia de gráficos de última generación?

¿En qué consiste el pensamiento algorítmico?

¿Cuáles son los trabajos orientados a la lógica pura para los programadores?

¿Por qué y cómo son importantes los algoritmos en nuestra vida diaria?

¿Por qué el algoritmo Chandy-Lamport necesita suponer que todos los mensajes llegan exactamente una vez?

¿Cuál es la solución eficiente para SPOJ CCROSSX?

¿TFIDF es una métrica para medir qué tan informativa es una palabra o un algoritmo de aprendizaje automático?

Cómo calcular el número 50 usando números binarios

¿Se puede resolver Codeforces 431C - k-tree por recusión?

¿Es mejor aprender primero los algoritmos y luego buscar problemas o simplemente elegir un problema aleatorio y luchar?

¿Cuál es el curso / certificación mejor pagado disponible para estructuras de datos y algoritmo?

¿Cómo es coherente una búsqueda iterativa de profundización en beneficio de BFS y DFS?

¿Es posible construir un algoritmo (para ejecutar en una computadora con recursos de espacio finito) que tomará como entrada un flujo de lanzamientos de monedas al azar imparciales (probabilidad independiente de caras 1/2) y emitirá caras con probabilidad irracional esperada?