Articulo de referencia

Recursión (informática)

Patrón de copo de nieve creado mediante recursión. Patrón de panal de abeja creado mediante recursión. Fractal de árbol creado utilizando el lenguaje de programación Logo. Dibuj...

Patrón de copo de nieve creado mediante recursión.
Patrón de panal de abeja creado mediante recursión.
Fractal de árbol creado utilizando el lenguaje de programación Logo.
Dibujo recursivo de un triángulo de Sierpiński mediante gráficos de tortuga.
Ejemplos realizados mediante recursión

En informática , la recursión es un método para resolver un problema computacional cuya solución depende de las soluciones de instancias más pequeñas del mismo problema. [ 1 ] [ 2 ] La recursión resuelve estos problemas recursivos mediante el uso de funciones que se llaman a sí mismas desde su propio código. Este enfoque se puede aplicar a muchos tipos de problemas, y la recursión es una de las ideas centrales de la informática. [ 3 ]

El poder de la recursión reside, evidentemente, en la posibilidad de definir un conjunto infinito de objetos mediante una instrucción finita . Del mismo modo, un número infinito de cálculos puede describirse mediante un programa recursivo finito, incluso si este programa no contiene repeticiones explícitas.

Niklaus Wirth , Algoritmos + Estructuras de datos = Programas , 1976 [ 4 ]

La mayoría de los lenguajes de programación informática admiten la recursión al permitir que una función se llame a sí misma desde dentro de su propio código. Algunos lenguajes de programación funcional (por ejemplo, Clojure ) [ 5 ] no definen ninguna construcción de bucle incorporada y, en cambio, se basan únicamente en la recursión. En la teoría de la computabilidad se demuestra que estos lenguajes basados ​​solo en la recursión son Turing completos ; esto significa que son tan potentes (pueden usarse para resolver los mismos problemas) como los lenguajes imperativos basados ​​en estructuras de control como whiley for.

Llamar repetidamente a una función desde dentro de sí misma puede provocar que la pila de llamadas alcance un tamaño igual a la suma de los tamaños de entrada de todas las llamadas involucradas. Por consiguiente, para problemas que se pueden resolver fácilmente mediante iteración, la recursión suele ser menos eficiente y, para ciertos problemas, las técnicas de optimización algorítmica o del compilador, como la optimización de llamadas recursivas, pueden mejorar el rendimiento computacional en comparación con una implementación recursiva simple.

Historia

El desarrollo de la recursión en la informática surgió de la lógica matemática y posteriormente se convirtió en una parte esencial del diseño de lenguajes de programación . [ 6 ] El trabajo inicial realizado por Church , Gödel , Kleene y Turing sobre la función recursiva y la computabilidad sentó las bases que hicieron posible la recursión en los lenguajes de programación. [ 6 ] Los matemáticos han utilizado la recursión durante mucho tiempo, pero solo se convirtió en una herramienta práctica para la programación a finales de la década de 1950 y principios de la de 1960. [ 7 ] Figuras clave como John McCarthy y el comité de diseño de ALGOL 60 contribuyeron a la introducción de la recursión en la programación. [ 7 ]

John McCarthy dio los primeros pasos al crear el lenguaje de programación LISP en 1960. [ 8 ] En su artículo «Funciones recursivas de expresiones simbólicas y su computación por máquina, Parte I» , McCarthy demostró que la recursión podía ser fundamental en un lenguaje de programación que trabajara con símbolos procesándolos paso a paso. [ 8 ] En LISP, la recursión podía utilizarse en funciones mediante reglas sencillas, y también existía una forma de evaluarlas en el lenguaje. [ 8 ] Esto demostró que la recursión era una forma práctica de escribir programas y que también describía el proceso de computación. [ 6 ] Por lo tanto, LISP se convirtió en uno de los primeros lenguajes de programación en utilizar la recursión como característica principal, e influyó posteriormente en otros lenguajes que le siguieron. [ 7 ]

Durante ese tiempo, también se añadió la recursión a ALGOL 60. [ 9 ] El Informe sobre el Lenguaje Algorítmico ALGOL 60 , publicado en 1960, fue el resultado de un intento internacional de diseñar un lenguaje estándar. [ 9 ] Permitir que los procedimientos se llamaran a sí mismos fue una de las nuevas características importantes del lenguaje. [ 7 ] Antes de eso, los programadores solo podían usar bucles , por lo que fue un cambio significativo. [ 7 ] La recursión permitió a los programadores describir algoritmos de una manera más natural y flexible. [ 7 ]

Estructura de una función recursiva

La definición de una función recursiva se divide típicamente en dos partes: uno o más casos base y uno o más casos recursivos. [ 10 ] Esta estructura refleja la lógica de la inducción matemática , que es una técnica de demostración donde probar los casos base y el paso inductivo garantiza que un teorema dado se cumple para todas las entradas válidas.

Caso base

El caso base especifica los valores de entrada para los cuales la función puede proporcionar un resultado directamente, sin recursión adicional. [ 11 ] Estos suelen ser los valores de entrada más simples o pequeños posibles (que se pueden resolver trivialmente), lo que permite que el cálculo finalice. Los casos base son esenciales porque evitan la regresión infinita. En otras palabras, definen una condición de parada que finaliza la recursión.

Un ejemplo es el cálculo del factorial de un entero n, que es el producto de todos los enteros desde 0 hasta n. Para este problema, la definición 0! = 1 es un caso base. Sin ella, la recursión podría continuar indefinidamente, lo que provocaría errores de no terminación o incluso desbordamiento de pila en implementaciones reales.

Diseñar un caso base correcto es crucial tanto por razones teóricas como prácticas. Algunos problemas tienen un caso base natural (por ejemplo, la lista vacía es un caso base en algunas funciones recursivas de procesamiento de listas), mientras que otros requieren un parámetro adicional para proporcionar un criterio de parada (por ejemplo, usar un contador de profundidad en el recorrido recursivo de árboles ).

En la programación recursiva , omitir el caso base o definirlo incorrectamente puede provocar una recursión infinita no deseada. En un estudio, los investigadores demostraron que muchos estudiantes tienen dificultades para identificar los casos base adecuados. [ 11 ]

Caso recursivo

El caso recursivo describe cómo dividir un problema en subproblemas más pequeños de la misma forma. [ 11 ] Cada paso recursivo transforma la entrada de manera que se aproxime a un caso base, asegurando el progreso hacia la terminación. Si el paso de reducción no logra progresar hacia un caso base, el algoritmo puede quedar atrapado en un bucle infinito .

En el ejemplo del factorial , el caso recursivo se define como: norte¡=norte(norte1)¡,norte>0.{\displaystyle n!=n\cdot (n-1)!\,,\forall n>0.} Aquí, cada invocación de la función disminuye la entrada.norte{\displaystyle n}por 1. De este modo, se asegura de que la recursión finalmente alcance el caso base denorte=0{\displaystyle n=0}.

El caso recursivo es análogo al paso inductivo en una demostración por inducción : parte de la premisa de que la función funciona para una instancia menor y luego extiende esta premisa a la entrada actual. Por lo tanto, las definiciones y algoritmos recursivos guardan un estrecho paralelismo con los argumentos inductivos en matemáticas , y su corrección a menudo se basa en técnicas de razonamiento similares.

Ejemplos

Existen diversas aplicaciones prácticas de la recursión que siguen la estructura de caso base y caso recursivo. Estas se utilizan ampliamente para resolver problemas complejos en informática.

Tipos de datos recursivos

Muchos programas informáticos deben procesar o generar una cantidad arbitrariamente grande de datos . La recursión es una técnica para representar datos cuyo tamaño exacto desconoce el programador : este puede especificar dichos datos mediante una definición autorreferencial . Existen dos tipos de definiciones autorreferenciales: inductivas y coinductivas .

Datos definidos inductivamente

Una definición de datos recursiva definida inductivamente es aquella que especifica cómo construir instancias de los datos. Por ejemplo, las listas enlazadas se pueden definir inductivamente (aquí, usando la sintaxis de Haskell ):

datos ListaDeCadenas = ListaVacía | Cons Cadena ListaDeCadenas

El código anterior especifica una lista de cadenas que puede estar vacía o ser una estructura que contiene una cadena y una lista de cadenas. La autorreferencia en la definición permite la construcción de listas de cualquier número (finito) de cadenas.

Otro ejemplo de definición inductiva son los números naturales (o enteros positivos ):

Un número natural es 1 o n+1, donde n es un número natural.

De forma similar, las definiciones recursivas se utilizan a menudo para modelar la estructura de expresiones y sentencias en lenguajes de programación. Los diseñadores de lenguajes suelen expresar las gramáticas en una sintaxis como la de Backus-Naur ; aquí se muestra una gramática de este tipo para un lenguaje sencillo de expresiones aritméticas con multiplicación y suma:

< expr > ::= < número > | ( < expr > * < expr > ) | ( < expr > + < expr > ) 

Esto indica que una expresión es un número, el producto de dos expresiones o la suma de dos expresiones. Al referirse recursivamente a las expresiones en la segunda y tercera línea, la gramática permite expresiones aritméticas arbitrariamente complejas, como (5 * ((3 * 6) + 8)), con más de una operación de producto o suma en una sola expresión.

Datos definidos de forma coinductiva y correacción

Una definición de datos coinductiva es aquella que especifica las operaciones que se pueden realizar sobre un dato; normalmente, las definiciones coinductivas autorreferenciales se utilizan para estructuras de datos de tamaño infinito.

Una definición coinductiva de secuencias infinitas de cadenas, dada de manera informal, podría verse así:

Una secuencia de cadenas es un objeto s tal que: cabeza(s) es una cadena, y tail(s) es una secuencia de cadenas.

Esto es muy similar a una definición inductiva de listas de cadenas; la diferencia es que esta definición especifica cómo acceder al contenido de la estructura de datos —a saber, a través de las funciones de accesohead y— y tailcuál puede ser ese contenido, mientras que la definición inductiva especifica cómo crear la estructura y de qué se puede crear.

La corecursión está relacionada con la coinducción y puede utilizarse para calcular instancias particulares de objetos (posiblemente) infinitos. Como técnica de programación, se usa con mayor frecuencia en el contexto de lenguajes de programación perezosos y puede ser preferible a la recursión cuando se desconoce el tamaño o la precisión deseados de la salida de un programa. En tales casos, el programa requiere tanto una definición para un resultado infinitamente grande (o infinitamente preciso) como un mecanismo para tomar una porción finita de ese resultado. El problema de calcular los primeros n números primos es uno que puede resolverse con un programa corecursivo (por ejemplo, aquí ).

Tipos de recursión

Recursión simple y recursión múltiple

La recursión que contiene una sola autorreferencia se conoce comorecursión simple , mientras que la recursión que contiene múltiples autorreferencias se conoce comorecursión múltiple . Los ejemplos estándar de recursión simple incluyen el recorrido de listas, como en una búsqueda lineal, o el cálculo de la función factorial, mientras que los ejemplos estándar de recursión múltiple incluyenel recorrido de árboles, como en una búsqueda en profundidad.

La recursión simple suele ser mucho más eficiente que la recursión múltiple y, por lo general, puede sustituirse por un cálculo iterativo que se ejecuta en tiempo lineal y requiere espacio constante. La recursión múltiple, en cambio, puede requerir tiempo y espacio exponenciales y es fundamentalmente recursiva, ya que no puede sustituirse por iteración sin una pila explícita.

En ocasiones, la recursión múltiple puede convertirse en recursión simple (y, si se desea, posteriormente en iteración). Por ejemplo, si bien el cálculo de la secuencia de Fibonacci implica ingenuamente múltiples iteraciones, ya que cada valor requiere dos valores previos, puede calcularse mediante recursión simple pasando dos valores sucesivos como parámetros.

Recursión indirecta

La mayoría de los ejemplos básicos de recursión, y la mayoría de los ejemplos presentados aquí, demuestranLa recursión directa se produce cuando una función se llama a sí misma. La recursión indirecta ocurre cuando una función no es llamada por sí misma, sino por otra función a la que llamó (directa o indirectamente). Por ejemplo, si f llama a f, se trata de recursión directa; pero si f llama a g, que a su vez llama a f, entonces se trata de recursión indirecta de f. Son posibles cadenas de tres o más funciones; por ejemplo, la función 1 llama a la función 2, la función 2 llama a la función 3, y la función 3 vuelve a llamar a la función 1.

La recursión indirecta también se denomina recursión mutua , un término más simétrico, aunque se trata simplemente de una diferencia de énfasis, no de un concepto distinto. Es decir, si f llama a g y luego g llama a f, que a su vez vuelve a llamar a g , desde el punto de vista de f , f es indirectamente recursiva, mientras que desde el punto de vista de g , también lo es indirectamente, y desde el punto de vista de ambas, f y g son mutuamente recursivas. De forma similar, un conjunto de tres o más funciones que se llaman entre sí puede denominarse conjunto de funciones mutuamente recursivas.

Recursión anónima

La recursión se suele realizar llamando explícitamente a una función por su nombre. Sin embargo, también se puede realizar llamando implícitamente a una función en función del contexto actual, lo cual es especialmente útil para funciones anónimas y se conoce como recursión anónima .

Recursión estructural versus generativa

Algunos autores clasifican la recursión como "estructural" o "generativa". La distinción radica en de dónde obtiene un procedimiento recursivo los datos con los que trabaja y cómo los procesa.

Las funciones que consumen datos estructurados suelen descomponer sus argumentos en sus componentes estructurales inmediatos y luego procesarlos. Si alguno de los componentes inmediatos pertenece a la misma clase de datos que la entrada, la función es recursiva. Por ello, nos referimos a estas funciones como FUNCIONES RECURSIVAS (ESTRUCTURALMENTE).

Felleisen, Findler, Flatt y Krishnaurthi, Cómo diseñar programas , 2001 [ 14 ]

Así, la característica definitoria de una función recursiva estructural es que el argumento de cada llamada recursiva es el contenido de un campo de la entrada original. La recursión estructural abarca casi todos los recorridos de árboles, incluyendo el procesamiento de XML, la creación y búsqueda de árboles binarios, etc. Al considerar la estructura algebraica de los números naturales (es decir, un número natural es cero o el sucesor de un número natural), funciones como el factorial también pueden considerarse recursión estructural.

La recursión generativa es la alternativa:

Muchos algoritmos recursivos conocidos generan un dato completamente nuevo a partir de los datos dados y lo procesan recursivamente. HtDP ( How to Design Programs ) se refiere a este tipo como recursión generativa. Algunos ejemplos de recursión generativa son: el máximo común divisor (MCD) , el algoritmo de ordenación rápida (Quicksort) , la búsqueda binaria , el algoritmo de ordenación por fusión (Mergesort) , el método de Newton , los fractales y la integración adaptativa .

Matthias Felleisen, Programación funcional avanzada , 2002 [ 15 ]

Esta distinción es importante para demostrar la terminación de una función.

  • Se puede demostrar fácilmente, mediante inducción estructural , que todas las funciones recursivas estructurales sobre estructuras de datos finitas ( definidas inductivamente ) terminan : intuitivamente, cada llamada recursiva recibe una porción más pequeña de datos de entrada, hasta que se alcanza un caso base.
  • En cambio, las funciones generativamente recursivas no necesariamente alimentan sus llamadas recursivas con datos de entrada cada vez más pequeños, por lo que demostrar su terminación no es tan sencillo y evitar bucles infinitos requiere mayor cuidado. Estas funciones generativamente recursivas a menudo pueden interpretarse como funciones correcursivas: cada paso genera nuevos datos, como la aproximación sucesiva en el método de Newton, y para terminar esta correcursión se requiere que los datos finalmente satisfagan alguna condición, lo cual no está necesariamente garantizado.
  • En términos de variantes de bucle , la recursión estructural se da cuando existe una variante de bucle obvia, concretamente en tamaño o complejidad, que comienza siendo finita y disminuye en cada paso recursivo.
  • Por el contrario, la recursión generativa se da cuando no existe una variante de bucle tan obvia, y la terminación depende de una función, como el "error de aproximación", que no necesariamente disminuye a cero, por lo que la terminación no está garantizada sin un análisis adicional.

Problemas de implementación

En la implementación práctica, en lugar de una función puramente recursiva (una sola comprobación para el caso base, de lo contrario paso recursivo), se pueden realizar varias modificaciones con fines de claridad o eficiencia. Estas incluyen:

  • Función de envoltura (en la parte superior)
  • Cortocircuitando el caso base, también conocido como "recursión de alcance" (al final).
  • Algoritmo híbrido (abajo): cambia a un algoritmo diferente una vez que los datos son lo suficientemente pequeños.

En aras de la elegancia, las funciones envolventes suelen ser bien vistas, mientras que la omisión del caso base es mal vista, especialmente en el ámbito académico. Los algoritmos híbridos se utilizan a menudo para mejorar la eficiencia, reduciendo la sobrecarga de la recursión en casos pequeños, y la recursión a distancia es un caso particular de esto.

Función de envoltura

Una función contenedora es una función que se llama directamente pero que no realiza la recursión por sí misma, sino que llama a una función auxiliar separada que es la que realmente realiza la recursión.

Las funciones de envoltura se pueden usar para validar parámetros (de modo que la función recursiva pueda omitirlos), realizar la inicialización (asignar memoria, inicializar variables), particularmente para variables auxiliares como el "nivel de recursión" o cálculos parciales para la memorización , y manejar excepciones y errores. En lenguajes que admiten funciones anidadas , la función auxiliar puede anidarse dentro de la función de envoltura y usar un ámbito compartido. En ausencia de funciones anidadas, las funciones auxiliares son en cambio una función separada, si es posible privada (ya que no se llaman directamente), y la información se comparte con la función de envoltura usando paso por referencia .

Cortocircuitando el caso base

La recursión de cortocircuito, también conocida como recursión de alcance limitado , consiste en comprobar el caso base antes de realizar una llamada recursiva; es decir, comprobar si la siguiente llamada será el caso base, en lugar de llamar a la función y luego comprobarlo. El cortocircuito se realiza principalmente por razones de eficiencia, para evitar la sobrecarga de una llamada a función que devuelve un valor inmediatamente. Cabe destacar que, dado que el caso base ya se ha comprobado (inmediatamente antes del paso recursivo), no es necesario comprobarlo por separado, pero sí se debe utilizar una función envoltorio para el caso en que la recursión general comience con el propio caso base. Por ejemplo, en la función factorial, el caso base correcto es 0! = 1, mientras que devolver inmediatamente 1 para 1! es un cortocircuito y puede omitir el 0; esto se puede mitigar mediante una función envoltorio. El recuadro muestra código C para atajar los casos 0 y 1 del factorial.

El cortocircuito es una preocupación principal cuando se encuentran muchos casos base, como los punteros nulos en un árbol, que pueden ser lineales en el número de llamadas a funciones, lo que supone un ahorro significativo para los algoritmos O ( n ) ; esto se ilustra a continuación para una búsqueda en profundidad. El cortocircuito en un árbol corresponde a considerar una hoja (nodo no vacío sin hijos) como caso base, en lugar de considerar un nodo vacío como caso base. Si solo hay un caso base, como en el cálculo del factorial, el cortocircuito proporciona un ahorro de solo O (1) .

Conceptualmente, el cortocircuito puede considerarse como tener el mismo caso base y paso recursivo, verificando el caso base solo antes de la recursión, o puede considerarse como tener un caso base diferente (un paso separado del caso base estándar) y un paso recursivo más complejo, a saber, "verificar válido y luego recursar", como al considerar nodos hoja en lugar de nodos nulos como casos base en un árbol. Debido a que el cortocircuito tiene un flujo más complicado, en comparación con la clara separación del caso base y el paso recursivo en la recursión estándar, a menudo se considera un estilo deficiente, particularmente en el ámbito académico. [ 16 ]

Un ejemplo básico de cortocircuito se da en la búsqueda en profundidad (DFS) de un árbol binario; consulte la sección de árboles binarios para una discusión recursiva estándar.

El algoritmo recursivo estándar para una búsqueda en profundidad (DFS) es:

  • Caso base: Si el nodo actual es nulo, devuelve falso.
  • Paso recursivo: de lo contrario, comprueba el valor del nodo actual, devuelve verdadero si coincide, de lo contrario recursa sobre los hijos.

En caso de cortocircuito, esto es en cambio:

  • verificar el valor del nodo actual, devolver verdadero si coincide,
  • De lo contrario, en los hijos, si no es Null, entonces recursión.

En cuanto a los pasos estándar, esto sitúa la comprobación del caso base antes del paso recursivo. Alternativamente, estos pueden considerarse, respectivamente, como una forma diferente de comprobación del caso base y del paso recursivo. Cabe destacar que esto requiere una función auxiliar para gestionar el caso en que el árbol esté vacío (el nodo raíz es nulo).

En el caso de un árbol binario perfecto de altura h, hay 2 h +1 1 nodos y 2 h +1 punteros nulos como hijos (2 por cada una de las 2 h hojas), por lo que el cortocircuito reduce a la mitad el número de llamadas a funciones en el peor de los casos.

En C, el algoritmo recursivo estándar se puede implementar de la siguiente manera:

bool tree_contains ( struct BinaryTree * node , int i ) { if ( ! node ) { return false ; // caso base } else if ( node- > data == i ) { return true ; } else { return tree_contains ( node- > left , i ) || tree_contains ( node- > right , i ); } }

El algoritmo de cortocircuito puede implementarse de la siguiente manera:

#incluir <assert.h>// Función envoltorio para manejar árboles vacíos bool tree_contains ( struct BinaryTree * node , int i ) { if ( ! node ) { return false ; // árbol vacío } else { return tree_contains_aux ( node , i ); // llamar a la función auxiliar } }// Se asume que nodo != NULL bool tree_contains_aux ( struct BinaryTree * nodo , int i ) { assert ( nodo ); if ( nodo -> datos == i ) { return true ; // encontrado } else { // recursión return ( nodo -> izquierda && tree_contains_aux ( nodo -> izquierda , i )) || ( nodo -> derecha && tree_contains_aux ( nodo -> derecha , i )); } }

Nótese el uso de la evaluación de cortocircuito de los operadores booleanos && ( AND), de modo que la llamada recursiva se realiza solo si el nodo es válido (no nulo). Nótese que, si bien el primer término del AND es un puntero a un nodo, el segundo término es un booleano, por lo que la expresión global se evalúa como un booleano. Este es un modismo común en la evaluación de cortocircuito recursiva. Esto se suma a la evaluación de cortocircuito del operador booleano || (OR), para comprobar el hijo derecho solo si el hijo izquierdo falla. De hecho, todo el flujo de control de estas funciones se puede reemplazar con una sola expresión booleana en una instrucción de retorno, pero la legibilidad se ve afectada sin que ello beneficie a la eficiencia.

Algoritmo híbrido

Los algoritmos recursivos suelen ser ineficientes para conjuntos de datos pequeños, debido a la sobrecarga que suponen las llamadas y devoluciones repetidas de funciones. Por este motivo, las implementaciones eficientes de algoritmos recursivos suelen comenzar con el algoritmo recursivo, pero luego cambian a otro algoritmo cuando la entrada se reduce. Un ejemplo importante es el ordenamiento por fusión (merge sort) , que a menudo se implementa cambiando al ordenamiento por inserción no recursivo cuando los datos son suficientemente pequeños, como en el ordenamiento por fusión en mosaico (tilesed merge sort ). Los algoritmos recursivos híbridos a menudo se pueden refinar aún más, como en Timsort , derivado de un ordenamiento por fusión/inserción híbrido.

Recursión versus iteración

La recursión y la iteración son igualmente expresivas: la recursión puede reemplazarse por la iteración con una pila de llamadas explícita , mientras que la iteración puede reemplazarse por la recursión de cola . El enfoque preferible depende del problema en cuestión y del lenguaje utilizado. En la programación imperativa , se prefiere la iteración, sobre todo para la recursión simple, ya que evita la sobrecarga de las llamadas a funciones y la gestión de la pila de llamadas, pero la recursión se utiliza generalmente para la recursión múltiple. Por el contrario, en los lenguajes funcionales se prefiere la recursión, y la optimización de la recursión de cola genera poca sobrecarga. Implementar un algoritmo mediante iteración puede no ser sencillo.

Compara las plantillas para calcular x n definido por x n = f(n, x n-1 ) a partir de x base :

En un lenguaje imperativo, la sobrecarga consiste en definir la función, y en un lenguaje funcional, la sobrecarga consiste en definir la variable acumuladora x.

Por ejemplo, una función factorial puede implementarse iterativamente en C asignando valores a una variable de índice de bucle y a una variable acumuladora, en lugar de pasar argumentos y devolver valores mediante recursión:

factorial sin signo ( entero sin signo n ) { producto sin signo = 1 ; // producto vacío es 1 mientras ( n > 0 ) { producto * = n ; --n ; } return producto ; }

Poder expresivo

La mayoría de los lenguajes de programación actuales permiten la especificación directa de funciones y procedimientos recursivos. Cuando se llama a una función de este tipo, el entorno de ejecución del programa realiza un seguimiento de las distintas instancias de la función (a menudo mediante una pila de llamadas , aunque pueden utilizarse otros métodos). Toda función recursiva puede transformarse en una función iterativa sustituyendo las llamadas recursivas por estructuras de control iterativas y simulando la pila de llamadas con una pila gestionada explícitamente por el programa. [ 17 ] [ 18 ]

Por el contrario, todas las funciones y procedimientos iterativos que pueden ser evaluados por una computadora (ver Completitud de Turing ) pueden expresarse en términos de funciones recursivas; las construcciones de control iterativas como los bucles while y for se reescriben rutinariamente en forma recursiva en lenguajes funcionales . [ 19 ] [ 20 ] Sin embargo, en la práctica esta reescritura depende de la eliminación de llamadas de cola , que no es una característica de todos los lenguajes. C , Java y Python son lenguajes principales notables en los que todas las llamadas a funciones, incluidas las llamadas de cola , pueden causar asignación de pila que no ocurriría con el uso de construcciones de bucle; en estos lenguajes, un programa iterativo funcional reescrito en forma recursiva puede desbordar la pila de llamadas , aunque la eliminación de llamadas de cola puede ser una característica que no está cubierta por la especificación de un lenguaje, y diferentes implementaciones del mismo lenguaje pueden diferir en las capacidades de eliminación de llamadas de cola.

Problemas de rendimiento

En lenguajes (como C y Java ) que favorecen las estructuras de bucle iterativas, los programas recursivos suelen tener un coste significativo en tiempo y espacio debido a la sobrecarga necesaria para gestionar la pila y la relativa lentitud de las llamadas a funciones; en los lenguajes funcionales , una llamada a una función (en particular una llamada recursiva ) suele ser una operación muy rápida, y la diferencia suele ser menos perceptible.

Como ejemplo concreto, la diferencia de rendimiento entre las implementaciones recursiva e iterativa del ejemplo del factorial anterior depende en gran medida del compilador utilizado. En lenguajes donde se prefieren las estructuras de bucle, la versión iterativa puede ser hasta varios órdenes de magnitud más rápida que la recursiva. En lenguajes funcionales, la diferencia de tiempo total entre ambas implementaciones puede ser insignificante; de ​​hecho, el coste de multiplicar primero los números mayores en lugar de los menores (como ocurre en la versión iterativa que se presenta aquí) puede anular cualquier ahorro de tiempo derivado de la iteración.

Espacio de pila

En algunos lenguajes de programación, el tamaño máximo de la pila de llamadas es mucho menor que el espacio disponible en el montón , y los algoritmos recursivos tienden a requerir más espacio en la pila que los algoritmos iterativos. En consecuencia, estos lenguajes a veces imponen un límite a la profundidad de la recursión para evitar desbordamientos de pila ; Python es uno de esos lenguajes. [ 21 ] Nótese la advertencia a continuación con respecto al caso especial de la recursión de cola .

Vulnerabilidad

Debido a que los algoritmos recursivos pueden estar sujetos a desbordamientos de pila, pueden ser vulnerables a entradas patológicas o maliciosas . [ 22 ] Algunos programas maliciosos atacan específicamente la pila de llamadas de un programa y aprovechan su naturaleza inherentemente recursiva. [ 23 ] Incluso en ausencia de malware, un desbordamiento de pila causado por recursión ilimitada puede ser fatal para el programa, y ​​la lógica de manejo de excepciones puede no impedir que el proceso correspondiente se termine . [ 24 ]

Multiplicar problemas recursivos

Los problemas recursivos múltiples son inherentemente recursivos, debido al estado previo que necesitan rastrear. Un ejemplo es el recorrido de árboles como en la búsqueda en profundidad ; aunque se utilizan métodos tanto recursivos como iterativos, [ 25 ] contrastan con el recorrido de listas y la búsqueda lineal en una lista, que es un método recursivo simple y, por lo tanto, naturalmente iterativo. Otros ejemplos incluyen algoritmos de divide y vencerás como Quicksort y funciones como la función de Ackermann . Todos estos algoritmos pueden implementarse iterativamente con la ayuda de una pila explícita , pero el esfuerzo del programador involucrado en administrar la pila y la complejidad del programa resultante, posiblemente superan cualquier ventaja de la solución iterativa.

Refactorización de la recursión

Los algoritmos recursivos pueden reemplazarse por contrapartes no recursivas. [ 26 ] Un método para reemplazar algoritmos recursivos es simularlos usando memoria heap en lugar de memoria stack . [ 27 ] Una alternativa es desarrollar un algoritmo de reemplazo basado completamente en métodos no recursivos, lo cual puede ser un desafío. [ 28 ] Por ejemplo, los algoritmos recursivos para la coincidencia de comodines , como el algoritmo wildmat de Rich Salz , [ 29 ] fueron típicos en el pasado. Se han desarrollado algoritmos no recursivos para el mismo propósito, como el algoritmo de coincidencia de comodines de Krauss , para evitar los inconvenientes de la recursión [ 30 ] y han mejorado solo gradualmente basándose en técnicas como la recopilación de pruebas y el perfilado del rendimiento. [ 31 ]

funciones recursivas de cola

Las funciones recursivas de cola son aquellas en las que todas las llamadas recursivas son de cola y, por lo tanto, no generan operaciones diferidas. Por ejemplo, la función mcd (que se muestra más abajo) es recursiva de cola. En cambio, la función factorial (también más abajo) no lo es ; dado que su llamada recursiva no se encuentra en posición de cola, genera operaciones de multiplicación diferidas que deben realizarse una vez que finaliza la última llamada recursiva. Con un compilador o intérprete que trate las llamadas recursivas de cola como saltos en lugar de llamadas a funciones, una función recursiva de cola como mcd se ejecutará utilizando espacio constante. Por lo tanto, el programa es esencialmente iterativo, equivalente a utilizar estructuras de control de lenguaje imperativo como los bucles "for" y "while".

La importancia de la recursión de cola radica en que, al realizar una llamada recursiva de cola (o cualquier llamada de cola), no es necesario guardar la posición de retorno de la función que realiza la llamada en la pila de llamadas ; cuando la llamada recursiva finaliza, se bifurca directamente en la posición de retorno previamente guardada. Por lo tanto, en lenguajes que reconocen esta propiedad de las llamadas de cola, la recursión de cola ahorra espacio y tiempo.

Orden de ejecución

Consideremos estas dos funciones:

Función 1

#include <stdio.h>void recursiveFunction ( int num ) { printf ( "%d \n " , num ); if ( num < 4 ) { recursiveFunction ( num + 1 ); } }

Función 2

#include <stdio.h>void recursiveFunction ( int num ) { if ( num < 4 ) { recursiveFunction ( num + 1 ); } printf ( "%d \n " , num ); }

El resultado de la función 2 es el mismo que el de la función 1, pero con las líneas intercambiadas.

En el caso de una función que se llama a sí misma solo una vez, las instrucciones colocadas antes de la llamada recursiva se ejecutan una vez por recursión antes que cualquiera de las instrucciones colocadas después de la llamada recursiva. Estas últimas se ejecutan repetidamente una vez que se ha alcanzado el número máximo de recursiones.

Tenga en cuenta también que el orden de las instrucciones de impresión está invertido, lo cual se debe a la forma en que las funciones y las instrucciones se almacenan en la pila de llamadas .

Procedimientos recursivos

Factorial

Un ejemplo clásico de procedimiento recursivo es la función utilizada para calcular el factorial de un número natural :

hecho(norte)={1si norte=0nortehecho(norte1)si norte>0{\displaystyle \operatorname {fact} (n)={\begin{cases}1&{\mbox{si }}n=0\\n\cdot \operatorname {fact} (n-1)&{\mbox{si }}n>0\\\end{cases}}}

La función también se puede escribir como una relación de recurrencia :

bnorte=nortebnorte1{\displaystyle b_{n}=nb_{n-1}}
b0=1{\displaystyle b_{0}=1}

Esta evaluación de la relación de recurrencia demuestra el cálculo que se realizaría al evaluar el pseudocódigo anterior:

Esta función factorial también se puede describir sin usar recursión, utilizando las estructuras de bucle típicas que se encuentran en los lenguajes de programación imperativos:

El código imperativo anterior es equivalente a esta definición matemática utilizando una variable acumuladora t :

hecho(norte)=Fadotadodo(norte,1)Fadotadodo(norte,t)={tsi norte=0Fadotadodo(norte1,nortet)si norte>0{\displaystyle {\begin{aligned}\operatorname {fact} (n)&=\operatorname {fact_{acc}} (n,1)\\\operatorname {fact_{acc}} (n,t)&={\begin{cases}t&{\mbox{si }}n=0\\\operatorname {fact_{acc}} (n-1,nt)&{\mbox{si }}n>0\\\end{cases}}\end{aligned}}}

La definición anterior se traduce fácilmente a lenguajes de programación funcional como Scheme ; este es un ejemplo de iteración implementada recursivamente.

Máximo común divisor

El algoritmo euclidiano , que calcula el máximo común divisor de dos números enteros, se puede escribir de forma recursiva.

Definición de la función :

mcd(incógnita,y)={incógnitasi y=0mcd(y,resto(incógnita,y))si y>0{\displaystyle \gcd(x,y)={\begin{cases}x&{\mbox{si }}y=0\\\gcd(y,\operatorname {resto} (x,y))&{\mbox{si }}y>0\\\end{cases}}}

Relación de recurrencia para el máximo común divisor, dondeincógnita%y{\displaystyle x\%y}expresa el resto deincógnita/y{\displaystyle x/y}:

mcd(incógnita,y)=mcd(y,incógnita%y){\displaystyle \gcd(x,y)=\gcd(y,x\%y)}siy0{\displaystyle y\neq 0}
mcd(incógnita,0)=incógnita{\displaystyle \gcd(x,0)=x}

El programa recursivo anterior es recursivo de cola ; es equivalente a un algoritmo iterativo, y el cálculo mostrado anteriormente ilustra los pasos de evaluación que realizaría un lenguaje que elimina las llamadas de cola. A continuación, se presenta una versión del mismo algoritmo que utiliza iteración explícita, adecuada para un lenguaje que no elimina las llamadas de cola. Al mantener su estado completamente en las variables x e y y utilizar una estructura de bucle, el programa evita realizar llamadas recursivas y aumentar la pila de llamadas.

El algoritmo iterativo requiere una variable temporal, e incluso conociendo el algoritmo euclidiano, es más difícil comprender el proceso mediante una simple inspección, aunque ambos algoritmos son muy similares en sus pasos.

Torres de Hanói

Torres de Hanói

Las Torres de Hanoi es un rompecabezas matemático cuya solución ilustra la recursión. [ 32 ] [ 33 ] Hay tres clavijas que pueden sostener pilas de discos de diferentes diámetros. Un disco más grande nunca puede apilarse sobre uno más pequeño. Comenzando con n discos en una clavija, deben moverse a otra clavija uno por uno. ¿Cuál es el número mínimo de pasos para mover la pila?

Definición de la función :

Hanoi(norte)={1si norte=12Hanoi(norte1)+1si norte>1{\displaystyle \operatorname {hanoi} (n)={\begin{cases}1&{\mbox{si }}n=1\\2\cdot \operatorname {hanoi} (n-1)+1&{\mbox{si }}n>1\\\end{cases}}}

Relación de recurrencia para Hanoi :

hnorte=2hnorte1+1{\displaystyle h_{n}=2h_{n-1}+1}
h1=1{\displaystyle h_{1}=1}

Ejemplos de implementación:

Aunque no todas las funciones recursivas tienen una solución explícita, la secuencia de la Torre de Hanoi se puede reducir a una fórmula explícita. [ 34 ]

El algoritmo de búsqueda binaria es un método para buscar un único elemento en un arreglo ordenado, dividiendo el arreglo por la mitad en cada iteración recursiva. El truco consiste en elegir un punto medio cercano al centro del arreglo, comparar los datos en ese punto con los datos que se buscan y, a continuación, responder a una de tres posibles condiciones: los datos se encuentran en el punto medio, los datos en el punto medio son mayores que los datos que se buscan, o los datos en el punto medio son menores que los datos que se buscan.

Este algoritmo utiliza la recursión porque, en cada iteración, se crea un nuevo array dividiendo el anterior por la mitad. A continuación, se llama recursivamente al procedimiento de búsqueda binaria, esta vez sobre el nuevo array (más pequeño). Normalmente, el tamaño del array se ajusta modificando un índice inicial y uno final. El algoritmo presenta un crecimiento logarítmico, ya que, en esencia, divide el dominio del problema por la mitad en cada iteración.

Ejemplo de implementación de búsqueda binaria en C:

/** * @brief Llama a binary_search con las condiciones iniciales adecuadas. * @param data un array de enteros ORDENADOS en orden ASCENDENTE * @param target el entero que se va a buscar * @param count el número total de elementos en el array * @returns el resultado de binary_search */ int search ( int data [], int target , int count ) { // Inicio = 0 (índice inicial) // Fin = count - 1 (índice superior) return binary_search ( data , target , 0 , count - 1 ); }/** * @brief Algoritmo de búsqueda binaria. * @param data un array de enteros ORDENADO en orden ASCENDENTE * @param target el entero a buscar * @param start el índice mínimo del array * @param end el índice máximo del array * @returns la posición del entero a encontrar dentro del array data, -1 si no se encuentra */ int binary_search ( int data [], int target , int start , int end ) { //Obtener el punto medio. int mid = start + ( end - start ) / 2 ; //División enteraif ( start > end ) { return -1 ; // Condición de parada (caso base) } else if ( data [ mid ] == target ) { return mid ; // Encontrado, devolver índice } else if ( data [ mid ] > target ) { // Los datos son mayores que el objetivo, buscar en la mitad inferior return binary_search ( data , target , start , mid - 1 ); } else { // Los datos son menores que el objetivo, buscar en la mitad superior return binary_search ( data , target , mid + 1 , end ); } }

Estructuras de datos recursivas (recursión estructural)

Una aplicación importante de la recursión en informática reside en la definición de estructuras de datos dinámicas, como listas y árboles . Las estructuras de datos recursivas pueden crecer dinámicamente hasta alcanzar un tamaño teóricamente infinito en respuesta a las necesidades de ejecución; en cambio, el tamaño de un array estático debe definirse en tiempo de compilación.

"Los algoritmos recursivos son particularmente apropiados cuando el problema subyacente o los datos a tratar se definen en términos recursivos." [ 35 ]

Los ejemplos de esta sección ilustran lo que se conoce como "recursión estructural". Este término se refiere al hecho de que los procedimientos recursivos actúan sobre datos definidos de forma recursiva.

Siempre que un programador derive la plantilla de una definición de datos, las funciones emplean recursión estructural. Es decir, las recursiones en el cuerpo de una función consumen alguna parte inmediata de un valor compuesto dado. [ 15 ]

Listas enlazadas

A continuación se muestra la definición en C de la estructura de un nodo de lista enlazada. Nótese especialmente cómo se define el nodo en términos de sí mismo. El elemento "next" LinkedListes un puntero a otra lista enlazada, creando así un tipo de lista.

struct LinkedList { int data ; // algunos datos enteros struct LinkedList * next ; // puntero a otro nodo de la lista enlazada };

Dado que la estructura de datos `struct node` se define recursivamente, los procedimientos que operan sobre ella pueden implementarse de forma natural como procedimientos recursivos. El procedimiento `list_print` que se define a continuación recorre la lista hasta que esta queda vacía (es decir, el puntero a la lista tiene el valor NULL). Para cada nodo, imprime el elemento de datos (un número entero). En la implementación en C, la lista permanece sin cambios tras la ejecución del procedimiento `list_print` .

void list_print ( struct LinkedList * list ) { // caso base if ( list ) { printf ( "%d " , list -> data ); // imprime datos enteros seguidos de un espacio list_print ( list -> next ); // llamada recursiva al siguiente nodo } }

Árboles binarios

A continuación se presenta una definición sencilla para un nodo de árbol binario. Al igual que el nodo de las listas enlazadas, se define recursivamente en términos de sí mismo. Existen dos punteros autorreferenciales: izquierdo (que apunta al subárbol izquierdo) y derecho (que apunta al subárbol derecho).

struct BinaryTree { int data ; // algunos datos enteros struct BinaryTree * left ; // puntero al subárbol izquierdo struct BinaryTree * right ; // puntero al subárbol derecho };

Las operaciones en el árbol se pueden implementar mediante recursión. Tenga en cuenta que, debido a que hay dos punteros autorreferenciales (izquierdo y derecho), las operaciones en el árbol pueden requerir dos llamadas recursivas:

// Comprueba si tree_node contiene i; devuelve 1 si es así, 0 si no. int tree_contains ( struct BinaryTree * node , int i ) { if ( ! node ) { return 0 ; // caso base } else if ( node -> data == i ) { return 1 ; } else { return tree_contains ( node -> left , i ) || tree_contains ( node -> right , i ); } }

Se realizarán como máximo dos llamadas recursivas para cualquier llamada dada a tree_contains tal como se definió anteriormente.

// Recorrido en orden: void tree_print ( struct BinaryTree * node ) { // caso base if ( node ) { tree_print ( node -> left ); // ir a la izquierda printf ( "%d " , node -> data ); // imprimir el entero seguido de un espacio tree_print ( node -> right ); // ir a la derecha } }

El ejemplo anterior ilustra un recorrido en orden de un árbol binario. Un árbol de búsqueda binaria es un caso especial de árbol binario donde los elementos de datos de cada nodo están ordenados.

Recorrido del sistema de archivos

Dado que el número de archivos en un sistema de archivos puede variar, la recursión es la única forma práctica de recorrerlo y, por lo tanto, enumerar su contenido. Recorrer un sistema de archivos es muy similar a recorrer un árbol ; por consiguiente, los conceptos que subyacen al recorrido de árboles son aplicables al recorrido de un sistema de archivos. Más concretamente, el siguiente código sería un ejemplo de recorrido en preorden de un sistema de archivos.

paquete org.wikipedia.examples ;import java.io.File ;public class Example { /**  * Obtiene las raíces del sistema de archivos  * Procede con el recorrido recursivo del sistema de archivos  */ private static void traverse () { File [] fs = File . listRoots (); for ( int i = 0 ; i < fs . length ; i ++ ) { System . out . println ( fs [ i ] ); if ( fs [ i ] . isDirectory () && fs [ i ] . canRead ()) { rtraverse ( fs [ i ] ); } } }/**  * Recorre recursivamente un directorio dado  *  * @param fd indica el punto de inicio del recorrido  */ private static void rtraverse ( File fd ) { File [] fss = fd . listFiles ();for ( int i = 0 ; i < fss . length ; i ++ ) { System . out . println ( fss [ i ] ); if ( fss [ i ] . isDirectory () && fss [ i ] . canRead ()) { rtraverse ( fss [ i ] ); } } }public static void main ( String [] args ) { traverse (); } }

Este código utiliza tanto recursión como iteración : se itera sobre los archivos y directorios, y cada directorio se abre de forma recursiva.

El método "rtraverse" es un ejemplo de recursión directa, mientras que el método "traverse" es una función envolvente.

El escenario "base" es que siempre habrá un número fijo de archivos y/o directorios en un sistema de archivos determinado.

Eficiencia temporal de los algoritmos recursivos

La eficiencia temporal de los algoritmos recursivos se puede expresar mediante una relación de recurrencia en notación Big O. Por lo general, se pueden simplificar a un único término Big-O.

Regla del atajo (teorema maestro)

Si la complejidad temporal de la función tiene la forma T(norte)=aT(norte/b)+F(norte){\displaystyle T(n)=a\cdot T(n/b)+f(n)}

Entonces, la notación Big O de la complejidad temporal es la siguiente:

  • SiF(norte)=O(norteregistrobaε){\displaystyle f(n)=O(n^{\log _{b}a-\varepsilon })}por alguna constanteε>0{\displaystyle \varepsilon >0}, entoncesT(norte)=Θ(norteregistroba){\displaystyle T(n)=\Theta (n^{\log _{b}a})}
  • SiF(norte)=Θ(norteregistroba){\displaystyle f(n)=\Theta (n^{\log _{b}a})}, entoncesT(norte)=Θ(norteregistrobaregistronorte){\displaystyle T(n)=\Theta (n^{\log _{b}a}\log n)}
  • SiF(norte)=Ω(norteregistroba+ε){\displaystyle f(n)=\Omega (n^{\log _{b}a+\varepsilon })}por alguna constanteε>0{\displaystyle \varepsilon >0}y siaF(norte/b)doF(norte){\displaystyle a\cdot f(n/b)\leq c\cdot f(n)}para alguna constante c < 1 y para todos los n suficientemente grandes , entoncesT(norte)=Θ(F(norte)){\displaystyle T(n)=\Theta (f(n))}

donde a representa el número de llamadas recursivas en cada nivel de recursión, b representa por qué factor menor es la entrada para el siguiente nivel de recursión (es decir, el número de partes en las que se divide el problema), y f ( n ) representa el trabajo que la función realiza independientemente de cualquier recursión (por ejemplo, partición, recombinación) en cada nivel de recursión.

Recursión en la programación lógica

En la interpretación procedimental de los programas lógicos , las cláusulas (o reglas) de la forma se tratan como procedimientos, que reducen los objetivos de la forma a subobjetivos de la forma . Por ejemplo, las cláusulas de Prolog :A:-BAB

ruta ( X , Y ) :- arco ( X , Y ). ruta ( X , Y ) :- arco ( X , Z ), ruta ( Z , Y ).

Defina un procedimiento que se puede usar para buscar un camino de X a Y , ya sea encontrando un arco directo de X a Y , o encontrando un arco de X a Z , y luego buscando recursivamente un camino de Z a Y. Prolog ejecuta el procedimiento razonando de arriba hacia abajo (o hacia atrás ) y buscando en profundidad el espacio de caminos posibles, una rama a la vez. Si intenta la segunda cláusula y no encuentra un camino de Z a Y , retrocede e intenta encontrar un arco de X a otro nodo, y luego busca un camino de ese otro nodo a Y.

Sin embargo, en la lectura lógica de los programas lógicos, las cláusulas se entienden declarativamente como condicionales cuantificados universalmente. Por ejemplo, la cláusula recursiva del procedimiento de búsqueda de caminos se entiende como la representación del conocimiento de que, para cada X , Y y Z , si existe un arco de X a Z y un camino de Z a Y, entonces existe un camino de X a Y. En forma simbólica:

incógnita,Y,Z(ardo(incógnita,Z)pagath(Z,Y)pagath(incógnita,Y)).{\displaystyle \forall X,Y,Z(arc(X,Z)\land path(Z,Y)\rightarrow path(X,Y)).}

La lectura lógica libera al lector de la necesidad de saber cómo se utiliza la cláusula para resolver problemas. La cláusula puede utilizarse de arriba hacia abajo, como en Prolog, para reducir problemas a subproblemas. O puede utilizarse de abajo hacia arriba (o hacia adelante ), como en Datalog , para derivar conclusiones a partir de condiciones. Esta separación de responsabilidades es una forma de abstracción que separa el conocimiento declarativo de los métodos de resolución de problemas (véase Algoritmo#Algoritmo = Lógica + Control ). [ 36 ]

Recursión infinita

Un error común entre los programadores es no proporcionar una forma de salir de una función recursiva, a menudo omitiendo o comprobando incorrectamente el caso base, lo que permite que se ejecute (al menos teóricamente) infinitamente al llamarse recursivamente sin cesar. Esto se denomina recursión infinita , y el programa nunca terminará. En la práctica, esto suele agotar el espacio de pila disponible . En la mayoría de los entornos de programación, un programa con recursión infinita no se ejecutará realmente para siempre. Eventualmente, algo fallará y el programa informará de un error. [ 37 ] Sin embargo, si se utiliza la optimización de llamadas de cola , las llamadas recursivas pueden optimizarse en un bucle infinito que puede ejecutarse indefinidamente.

A continuación se muestra un código Java que utilizaría recursión infinita:

clase pública InfiniteRecursion {estático void recursivo () {// Función recursiva sin salidarecursivo ();}public static void main ( String [] args ) {recursive (); // Ejecuta la función recursiva en tiempo de ejecución.}}

Al ejecutar este código se producirá un error de desbordamiento de pila .

Véase también

Notas

  1. Graham, Ronald ; Knuth, Donald ; Patashnik, Oren (1990). "1: Problemas recurrentes" . Matemáticas concretas . Addison-Wesley. ISBN 0-201-55802-5.
  2. Kuhail, Mohammad A.; Negreiros, Joao; Seffah, Ahmed (2021). "Enseñanza del pensamiento recursivo mediante actividades sin conexión a internet". World Transactions on Engineering and Technology Education . 19 (2): 169– 175.
  3. Epp, Susanna (1995). Matemáticas discretas con aplicaciones (2.ª ed.). PWS Publishing Company. pág . 427. ISBN   978-0-53494446-9.
  4. Wirth, Niklaus (1976). Algoritmos + Estructuras de datos = Programas . Prentice-Hall . pág . 126. ISBN  978-0-13022418-7.
  5. "Programación funcional | Clojure para los valientes y verdaderos" . www.braveclojure.com . Consultado el 21 de octubre de 2020 .
  6. 1 2 3 Soare, Robert I. (1996). "Computabilidad y recursión". Boletín de lógica simbólica . 2 (3): 284– 321. doi : 10.2307/420992 . JSTOR 420992 . 
  7. 1 2 3 4 5 6 Daylight, Edgar G. (2010). El advenimiento de la recursión en la programación, décadas de 1950 y 1960 (Informe). Serie de preimpresiones PP-2010-04. Universidad de Ámsterdam, Instituto de Lógica, Lenguaje y Computación.
  8. 1 2 3 McCarthy, John (1960). "Funciones recursivas de expresiones simbólicas y su cálculo por máquina, parte I". Communications of the ACM . 3 (4): 184– 195. doi : 10.1145/367177.367199 .
  9. 1 2 Naur, Peter; John Backus; John McCarthy (1960). "Informe sobre el lenguaje algorítmico ALGOL 60". Communications of the ACM . 3 (5): 299– 314. doi : 10.1145/367236.367262 .
  10. Kong, Qingkai; Siauw, Timmy; Bayen, Alexandre M. (2021). "Recursión". Programación en Python y métodos numéricos . págs. 105–120 . doi : 10.1016/B978-0-12-819549-9.00015-4 . ISBN  978-0-12-819549-9.
  11. 1 2 3 Sweigart, Al (2022). El libro recursivo de la recursión: Domina la entrevista de programación con Python y JavaScript . No Starch Press. ISBN 978-1-7185-0202-4.
  12. Hopcroft, John; Tarjan, Robert (junio de 1973). "Algoritmo 447: algoritmos eficientes para la manipulación de grafos". Communications of the ACM . 16 (6): 372– 378. doi : 10.1145/362248.362272 .
  13. Bron, C. (mayo de 1972). "Algoritmo 426: algoritmo de ordenación por fusión [M1]". Communications of the ACM . 15 (5): 357– 358. doi : 10.1145/355602.361317 .
  14. ^ Felleisen y col. 2001 , arte V “Recursión Generativa
  15. 1 2 Felleisen, Matthias (2002). "Desarrollo de programas web interactivos" . En Jeuring, Johan (ed.). Programación funcional avanzada: 4.ª Escuela Internacional (PDF) . Springer. pág. 108. ISBN  9783540448334.
  16. ↑ Mongan, John ; Giguère, Eric; Kindler, Noah (2013). Programming Interviews Exposed: Secrets to Landing Your Next Job (3.ª ed.). Wiley . p. 115. ISBN   978-1-118-26136-1.
  17. Hetland, Magnus Lie (2010), Python Algorithms: Mastering Basic Algorithms in the Python Language , Apress, p. 79, ISBN  9781430232384.
  18. ^ Drozdek, Adam (2012), Estructuras de datos y algoritmos en C++ (4ª ed.), Cengage Learning, p. 197, ISBN   9781285415017.
  19. Shivers, Olin. "La anatomía de un bucle: una historia de alcance y control" (PDF) . Instituto Tecnológico de Georgia . Consultado el 3 de septiembre de 2012 .
  20. Lambda the Ultimate. "La anatomía de un bucle" . Lambda the Ultimate . Consultado el 3 de septiembre de 2012 .
  21. "27.1. sys — Parámetros y funciones específicos del sistema — Documentación de Python v2.7.3" . Docs.python.org . Consultado el 3 de septiembre de 2012 .
  22. Krauss, Kirk J. (2014). "Matching Wildcards: An Empirical Way to Tame an Algorithm" . Dr. Dobb's Journal .
  23. Mueller, Oliver (2012). "Anatomía de un ataque de destrucción de pila y cómo GCC lo previene" . Dr. Dobb's Journal .
  24. "Clase StackOverflowException" . Biblioteca de clases de .NET Framework . Microsoft Developer Network . 2018.
  25. "Búsqueda en profundidad (DFS): Implementación iterativa y recursiva" . Techie Delight. 2018.
  26. Mitrovic, Ivan. "Reemplazar la recursión con la iteración" . ThoughtWorks .
  27. La, Woong Gyu (2015). "Cómo reemplazar funciones recursivas usando pila y bucle while para evitar el desbordamiento de pila" . CodeProject.
  28. Moertel, Tom (2013). "Trucos del oficio: De la recursión a la iteración, parte 2: Eliminando la recursión con el truco de la función secreta de viaje en el tiempo" .
  29. ^ Salz, rico (1991). "salvajemat.c" . GitHub .
  30. Krauss, Kirk J. (2008). "Matching Wildcards: An Algorithm" . Dr. Dobb's Journal .
  31. Krauss, Kirk J. (2018). "Matching Wildcards: An Improved Algorithm for Big Data" . Develop for Performance.
  32. Graham, Knuth y Patashnik 1990 , §1.1: La Torre de Hanoi
  33. Epp 1995 , págs. 427–430: La Torre de Hanoi 
  34. Epp 1995 , págs. 447–448: Una fórmula explícita para la secuencia de la Torre de Hanoi 
  35. Wirth 1976 , pág. 127 
  36. Russell, Stuart J .; Norvig, Peter. (2021). Inteligencia artificial: un enfoque moderno §9.3, §9.4 (4.ª ed.). Hoboken: Pearson. ISBN  978-0134610993. LCCN 20190474 . 
  37. "4.8. Recursión infinita: cómo pensar como un científico informático - C++" .

Referencias

  • Barron, David William (1968) [1967]. Escrito en Cambridge, Reino Unido. Gill, Stanley (ed.). Técnicas recursivas en programación . Macdonald Computer Monographs (1.ª  ed.). Londres, Reino Unido: Macdonald & Co. (Publishers) Ltd. SBN 356-02201-3.(viii+64 páginas)
  • Felleisen, Matthias; Findler, Robert B.; Flatt, Matthew; Krishnamurthi, Shriram (2001). Cómo diseñar programas: Una introducción a la informática y la programación . MIT Press . ISBN 0262062186.
  • Rubio-Sánchez, Manuel (2017). Introducción a la programación recursiva . CRC Press . ISBN 978-1-351-64717-5.
  • Pevac, Irena (2016). Practicando la recursividad en Java . Crear espacio independiente. ISBN 978-1-5327-1227-2.
  • Roberts, Eric (2005). Pensando recursivamente con Java . Wiley . ISBN 978-0-47170146-0.
  • Rohl, Jeffrey S. (1984). Recursion Via Pascal . Cambridge University Press . ISBN 978-0-521-26934-6.
  • Helman, Paul; Veroff, Robert. Paredes y espejos .
  • Abelson, Harold ; Sussman, Gerald Jay ; Sussman, Julie (1996). Estructura e interpretación de programas informáticos (2.ª  ed.). MIT Press . ISBN 0-262-51087-1.
  • Dijkstra, Edsger W. (1960). "Programación recursiva". Matemática numérica . 2 (1): 312– 318. doi : 10.1007/BF01386232 . S2CID 127891023 . 
  • McCarthy, John (1960). "Funciones recursivas de expresiones simbólicas y su cálculo por máquina, parte I". Communications of the ACM . 3 (4): 184– 195. doi : 10.1145/367177.367199 .
  • Naur, Peter; John Backus; John McCarthy (1960). "Informe sobre el lenguaje algorítmico ALGOL 60". Communications of the ACM . 3 (5): 299– 314. doi : 10.1145/367236.367262 .
  • Hoare, CAR (1961). "Quicksort". Communications of the ACM . 4 (7): 321– 322. doi : 10.1145/366622.366644 .
  • Soare, Robert I. (1996). "Computabilidad y recursión". Boletín de lógica simbólica . 2 (3): 284– 321. doi : 10.2307/420992 . JSTOR 420992 . 
  • Daylight, Edgar G. (2010). El advenimiento de la recursión en la programación, décadas de 1950 y 1960 (Informe). Serie de preimpresiones PP-2010-04. Universidad de Ámsterdam, Instituto de Lógica, Lenguaje y Computación.
  • Endres, Madeline; Westley Weimer; Amir Kamil (2021). "Análisis del rendimiento de problemas iterativos y recursivos". Actas del 52.º Simposio Técnico de la ACM sobre Educación en Ciencias de la Computación (SIGCSE '21) . Association for Computing Machinery. doi : 10.1145/3408877.3432391 .
  • Cormen, Thomas H.; Charles E. Leiserson; Ronald L. Rivest; Clifford Stein (2009). Introducción a los algoritmos (3ª  ed.). Prensa del MIT. ISBN 978-0-262-03384-8.