fbpx
Wikipedia

Recursión (ciencias de computación)

Para un tratamiento más general de los fenómenos recursivos, ver el artículo de Recursión.

Recursión es, en ciencias de la computación, una forma de atajar y solventar problemas. De hecho, recursión es una de las ideas centrales de ciencia de computación.[1]​ Resolver un problema mediante recursión significa que la solución depende de las soluciones de pequeñas instancias del mismo problema.[2]

El poder de la recursión evidentemente se fundamenta en la posibilidad de definir un conjunto infinito de objetos con una declaración finita. Igualmente, un número infinito de operaciones computacionales puede describirse con un programa recursivo finito, incluso en el caso de que este programa no contenga repeticiones explícitas."[3]

La mayoría de los lenguajes de programación dan soporte a la recursión permitiendo a una función llamarse a sí misma desde el texto del programa. Los lenguajes imperativos definen las estructuras de loops como while y for que son usadas para realizar tareas repetitivas. Algunos lenguajes de programación funcionales no definen estructuras de loops sino que posibilitan la recursión llamando código de forma repetitiva. La teoría de la computabilidad ha demostrado que estos dos tipos de lenguajes son matemáticamente equivalentes, es decir que pueden resolver los mismos tipos de problemas, aunque los lenguajes funcionales carezcan de las típicas estructuras while y for.

Árbol basado en la recursión creado usando el lenguaje de programación Logo.

Algoritmos recursivos

Un algoritmo recursivo es un algoritmo que expresa la solución un problema en términos de una llamada a sí mismo. La llamada a sí mismo se conoce como llamada recursiva o [Recursión (ciencias de computación)|recurrente].

Generalmente, si la primera llamada al subprograma se plantea sobre un problema de tamaño u orden N, cada nueva ejecución recurrente del mismo se planteará sobre problemas, de igual naturaleza que el original, pero de un tamaño menor que N. De esta forma, al ir reduciendo progresivamente la complejidad del problema que resolver, llegará un momento en que su resolución sea más o menos trivial (o, al menos, suficientemente manejable como para resolverlo de forma no recursiva). En esa situación diremos que estamos ante un caso base de la recursividad.

Las claves para construir un subprograma recurrente son:

  • Cada llamada recurrente se debería definir sobre un problema de menor complejidad (algo más fácil de resolver).
  • Ha de existir al menos un caso base para evitar que la recurrencia sea infinita.

Es frecuente que los algoritmos recurrentes sean más ineficientes en tiempo que los iterativos aunque suelen ser mucho más breves en espacio.

Un método frecuente para simplificar es dividir un problema en problemas derivados de menor tamaño del mismo tipo. Esto se conoce como dialecting. Como técnica de programación se denomina divide y vencerás y es pieza fundamental para el diseño de muchos algoritmos de importancia, así como parte esencial de la programación dinámica.

Virtualmente todos los lenguajes de programación modernos permiten la especificación directa de funciones y subrutinas recursivas. Cuando se llama una función de este tipo, el ordenador, para la mayoría de los lenguajes en casi todas las arquitecturas basadas en una pila (stack) o en la implementación del lenguaje, lleva la cuenta de las distintas instancias de la función, en numerosas arquitecturas mediante el uso de un call stack, aunque no de forma exclusiva. A la inversa, toda función recursiva puede transformarse en una función iterativa usando un stack.

La mayoría (aunque no todas) de las funciones y subrutinas que pueden ser evaluadas por un ordenador, pueden expresarse en términos de una función recursiva (sin tener que utilizar una iteración pura); a la inversa, cualquier función recursiva puede expresarse en términos de una iteración pura, dado que la recursión es, de por sí, también iterativa. Para evaluar una función por medio de la recursión, tiene que definirse como una función de sí misma (ej. el factor n! = n * (n - 1)! , donde 0! se define como 1). Resulta evidente que no todas las evaluaciones de funciones se prestan a un acercamiento recursivo. Por lo general, todas las funciones finitas pueden describirse directamente de forma recursiva; las funciones infinitas (ej. las series de e = 1/1! + 2/2! + 3/3!...) necesitan un criterio extra para detenerse, ej. el número de iteraciones, o el número de dígitos significativos, en caso contrario una iteración recursiva resultaría en un bucle infinito.

A modo de ilustración: Si se encuentra una palabra desconocida en un libro, el lector puede anotar la página actual en un papel y ponerlo en una pila (hasta entonces vacía). El lector consulta la palabra en otro artículo y, de nuevo, descubre otra palabra desconocida, la anota y la pone en la pila, y así sucesivamente. Llega un momento que el lector lee un artículo que donde todas las palabras son conocidas. El lector retorna entonces a la última página y continua la lectura desde ahí, y así hasta que se retira la última nota de la pila retornando entonces al libro original. Este modus operandi es recursivo.

Algunos lenguajes diseñados para programación lógica y programación funcional ofrecen la recursión como el único medio de repetición directa disponible para el programador. Estos lenguajes suelen conseguir que la recursión de cola sea tan eficiente como la iteración, permitiendo a los programadores expresar otras estructuras repetitivas (tales como map y for de scheme) en términos de recursión.

La recursión está profundamente anclada en la teoría de computación, con la equivalencia teórica de función microrecursiva y máquinas de Turing en la cimentación de ideas sobre la universalidad del ordenador moderno.

Programación recursiva

Crear una subrutina recursiva requiere principalmente la definición de un "caso base", y entonces definir reglas para subdividir casos más complejos en el caso base. Para una subrutina recursiva es esencial que con cada llamada recursiva, el problema se reduzca de forma que al final llegue al caso base.

Algunos expertos clasifican la recursión como "generativa" o bien "estructural". La distinción se hace según de donde provengan los datos con los que trabaja la subrutina. Si los datos proceden de una estructura de datos similar a una lista, entonces la subrutina es "estructuralmente recursiva"; en caso contrario, es "generativamente recursiva".[4]

Muchos algoritmos populares generan una nueva cantidad de datos a partir de los datos aportados y recurren a partir de ahí. HTDP (How To Design Programs), al español, "Cómo diseñar programas", se refiere a esta variante como recursión generativa. Ejemplos de recursión generativa incluyen: máximo común divisor, quicksort, búsqueda binaria, mergesort, Método de Newton, fractals e integración adaptiva.[5]

Ejemplos de subrutinas definidas recursivamente (recursión generativa)

Factorial

Un ejemplo clásico de una subrutina recursiva es la función usada para calcular el factorial de un entero.

Definición de la función:

 
Pseudocódigo (recursivo):
función factorial:
input: entero n de forma que n >= 0
output: [n × (n-1) × (n-2) × … × 1]
1. if n es 0, return 1 2. else, return [ n × factorial(n-1) ]
end factorial


Una relación recurrente es una ecuación que relaciona términos posteriores en la secuencia con términos previos.[6]

Relación recurrente de un factorial:

 
 
Computando la relación recurrente para n = 4:
b4 = 4 * b3
= 4 * 3 * b2 = 4 * 3 * 2 * b1 = 4 * 3 * 2 * 1 * b0 = 4 * 3 * 2 * 1 * 1 = 4 * 3 * 2 * 1 = 4 * 3 * 2 = 4 * 6 = 24


Esta función factorial también puede describirse sin usar recursión haciendo uso de típicas estructuras de bucle que se encuentran en lenguajes de programación imperativos:

Pseudocódigo (iterativo):
función factorial es:
input: entero n de forma que n >= 0
output: [n × (n-1) × (n-2) × … × 1]
1. crear una variable nueva llamada running_total con un valor = 1
2. begin loop 1. si n es = 0, salir del loop 2. cambiar running_total a (running_total × n) 3. decrementar n 4. repetir el loop
3. return running_total
end factorial

El lenguaje de programación scheme es, sin embargo, un lenguaje de programación funcional y no define estructuras de loops de cualquier tipo. Se basa únicamente en la recursión para ejecutar todo tipo de loops. Dado que scheme es recursivo de cola, se puede definir una subrutina recursiva que implementa la subrutina factorial como un proceso iterativo, es decir, usa espacio constante pero tiempo lineal.

Fibonacci

Otra popular secuencia recursiva es el Número de Fibonacci. Los primeros elementos de la secuencia son: 0, 1, 1, 2, 3, 5, 8, 13, 21...

Definición de la función:

 
Pseudocódigo
function fib is: input: entero n de forma que n >= 0 
1. si n es = 0, return 0 2. si n es = 1, return 1 3. else, return [ fib(n-1) + fib(n-2) ]
end fib

Relación recurrente para Fibonacci:
bn = bn-1 + bn-2
b1 = 1, b0 = 0

Computando la relación recurrente para n = 4:
 b4 = b3 + b2 = b2 + b1 + b1 + b0 = b1 + b0 + 1 + 1 + 0 = 1 + 0 + 1 + 1 + 0 = 3 

Este algoritmo de Fibonacci es especialmente malo pues cada vez que se ejecuta la función, realizará dos llamadas a la función a sí misma, cada una de las cuales hará a la vez dos llamadas más y así sucesivamente hasta que terminen en 0 o en 1. El ejemplo se denomina "recursión de árbol", y sus requisitos de tiempo crecen de forma exponencial y los de espacio de forma lineal.[7]

Máximo común divisor

Otro famosa función recursiva es el algoritmo de Euclides, usado para computar el máximo común divisor de dos enteros.

Definición de la función:

 
Pseudocódigo (recursivo):
function gcd is: input: entero x, entero y de forma que x >= y y y > 0 
1. if y is 0, return x 2. else, return [ gcd( y, (remainder of x/y) ) ]
end gcd

Relación recursiva del máximo común denominador, donde   expresa el resto de la división entera  :

 
 
Computando la relación recurrente para x = 27 e y = 9:
gcd(27, 9) = gcd(9, 27 % 9) = gcd(9, 0) = 9 
Computando la relación recurrente para x = 259 e y = 111:
gcd(259, 111) = gcd(111, 259 % 111) = gcd(111, 37) = gcd(37, 0) = 37 

Nótese que el algoritmo "recursivo" mostrado arriba es, de hecho, únicamente de cola recursiva, lo que significa que es equivalente a un algoritmo iterativo. En el ejemplo siguiente se muestra el mismo algoritmo usando explícitamente iteración. No acumula una cadena de operaciones deferred, sino que su estado es, más bien, mantenido completamente en las variables x e y. Su "number of steps grows the as the logarithm of the numbers involved. ",[8]​ al español "número de pasos crece a medida que lo hace el logaritmo de los números involucrados."

Pseudocódigo:
función gcd es:
input: entero x, entero y de forma que x >= y e y > 0
1. crear una nueva variable llamada remainder
2. begin loop 1. if y is zero, exit loop 2. set remainder to the remainder of x/y 3. set x to y 4. set y to remainder 5. repeat loop
3. return x
end gcd

El algoritmo iterativo requiere una variable temporal, e incluso supuesto el conocimiento del Algoritmo de Euclides es más difícil de entender el proceso a simple vista, aunque los dos algoritmos son muy similares en sus pasos.

Torres de Hanói

Para una detallada discusión de la descripción de este problema, de su historia y de su solución, consúltese el artículo principal.[9][10]​ El problema, puesto de forma simple, es el siguiente: Dadas 3 pilas, una con un conjunto de N discos de tamaño creciente, determina el mínimo (óptimo) número de pasos que lleva mover todos los discos desde su posición inicial a otra pila sin colocar un disco de mayor tamaño sobre uno de menor tamaño.

Definición de la función:

 

Relación de recurrencia para hanoi:

 
 
Computación de la relación de recurrencia para n = 4:
hanoi(4) = 2*hanoi(3) + 1 = 2*(2*hanoi(2) + 1) + 1 = 2*(2*(2*hanoi(1) + 1) + 1) + 1 = 2*(2*(2*1 + 1) + 1) + 1 = 2*(2*(3) + 1) + 1 = 2*(7) + 1 = 15 


Ejemplos de implementación:

Pseudocódigo (recursivo):
function hanoi is:
input: integer n, such that n >= 1
1. if n is 1 then return 1
2. return [2 * [call hanoi(n-1)] + 1]
end hanoi


Aunque no todas las funciones recursivas tienen una solución explícita, la secuencia de la Torre de Hanói puede reducirse a una fórmula explícita.[11]

Una fórmula explícita de las Torres de Hanói:
h1 = 1 = 21 - 1 h2 = 3 = 22 - 1 h3 = 7 = 23 - 1 h4 = 15 = 24 - 1 h5 = 31 = 25 - 1 h6 = 63 = 26 - 1 h7 = 127 = 27 - 1 
Por lo general: hn = 2n - 1, for all n >= 1 

Búsqueda binaria

El algoritmo de búsqueda binaria es un método de búsqueda de un dato en un vector de datos ordenado dividiendo el vector en dos tras cada pasada. El truco es escoger un punto cerca del centro del vector, comparar en ese punto el dato con el dato buscado para responder entonces a una de las siguientes 3 condiciones: se encuentra el dato buscado, el dato en el punto medio es mayor que el valor buscado o el dato en el punto medio es menor que el valor buscado.

Se usa recursión en este algoritmo porque tras cada pasada se crea un nuevo vector dividiendo en original en dos. La subrutina de búsqueda binaria se llama entonces de forma recursiva, cada vez con un vector de menor tamaño. El tamaño del vector se ajusta normalmente cambiando el índice inicial y final. El algoritmo muestra un orden logaritmo de crecimiento porque divide esencialmente el dominio del problema en dos tras cada pasada.

Ejemplo de implementación de la búsqueda binaria:

 /*  Call binary_search with proper initial conditions.    Entrada:   Los datos se presentan en forma de vector de [[número entero|números enteros]] ordenado de forma ascendente,  ''toFind'' es el número entero a buscar,  ''count'' es el número total de elementos del vector    Salida:  resultado de la búsqueda binaria    */  int search(int *data, int toFind, int count)  {  // Start = 0 (índice inicial)  // End = count - 1 (índice superior)  return binary_search(data, toFind, 0, count-1);  }  /*  Algoritmo de la búsqueda binaria.    Entrada:   Los datos se presentan en forma de vector de [[número entero|números enteros]] ordenado de forma ascendente,  ''toFind'' es el número entero a buscar,  ''start'' es el índice mínimo del vector,  ''end'' es el índice máximo del vector  Salida:   posición del número entero ''toFind'' dentro del vector de datos,   -1 en caso de búsqueda fallida  */  int binary_search(int *data, int toFind, int start, int end)  {  //Averigua el punto medio.  int mid = start + (end - start)/2; //División de enteros    //Condición para detenerse.  if (start > end)  return -1;  else if (data[mid] == toFind) //Encontrado?  return mid;  else if (data[mid] > toFind) //El dato es mayor que ''toFind'', se busca en la mitad inferior  return binary_search(data, toFind, start, mid-1);  else //El dato es menor que ''toFind'', se busca en la mitad superior  return binary_search(data, toFind, mid+1, end);  } 

Estructuras de datos recursivo (recursión estructural)

Una aplicación de importancia de la recursión en ciencias de la computación es la definición de estructuras de datos dinámicos tales como listas y árboles. Las estructuras de datos recursivos pueden crecer de forma dinámica hasta un tamaño teórico infinito en respuesta a requisitos del tiempo de ejecución; por su parte, los requisitos del tamaño de un vector estático deben declararse en el tiempo de complicación.

"Los algoritmos recursivos son especialmente apropiados cuando el problema que resolver o los datos que manejar son definidos en términos recursivos."[12]

Los ejemplos en esta sección ilustran lo que se conoce como "recursión estructural". Este término se refiere al hecho de que las subrutinas recursivas se aplican a datos que se definen de forma recursiva.

En la medida en que un programador deriva una 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 una determinada cantidad de un compuesto dado de forma inmediata.[13]

Listas enlazadas

A continuación se describe una definición simple del nodo de una lista enlazada. Nótese como se define el nodo por sí solo. El siguiente elemento del nodo del struct es un puntero a un nodo de struct.

struct node {  int n; // algún tipo de datos  struct node *next; // puntero a otro nodo de ''struct'' }; // LIST no es otra cosa que un nodo de ''struct'' *. typedef struct node *LIST; 

Las subrutinas que operan en la estructura de datos de LIST pueden implementarse de forma natural como una subrutina recursiva porque la estructura de datos sobre la que opera (LIST) es definida de forma recursiva. La subrutina printList definida a continuación recorre la lista hacia abajo hasta que ésta se vacía (NULL), para cada nodo imprime el dato (un número entero). En la implementación en C, la lista permanece inalterada por la subrutina printList.

void printList(LIST lst) {  if (!isEmpty(lst)) // caso básico  {  printf("%d ", lst->n); // imprime el entero seguido por un espacio  printList(lst->next); // llamada recursiva  } } 

Árboles binarios

Más abajo se muestra una definición simple de un nodo de árbol binario. Al igual que el nodo de listas enlazadas, se define a sí misma (de forma recursiva). Hay dos punteros que se refieren a sí mismos – left (apuntando a l aparte izquierda del subárbol) y right (a la parte derecha del subárbol).

struct node {  int n; // algún tipo de datos  struct node *left; // puntero al subárbol izquierdo  struct node *right; // puntero al subárbol derecho }; // TREE no es otra cosa que un nodo '' struct '' typedef struct node *TREE; 

Las operaciones en el árbol pueden implementarse usando recursión. Nótese que, debido al hecho de que hay dos punteros que se referencian a sí mismos (izquierda y derecha), esas operaciones del árbol van a necesitar dos llamadas recursivas. Para un ejemplo similar, véase la función de Fibonacci y la explicación siguiente.

void printTree(TREE t) {  if (!isEmpty(t)) { // caso básico   printTree(t->left); // ir a la izquierda  printf("%d ", t->n); // imprimir el entero seguido de un espacio  printTree(t->right); // ir a la derecha  } } 

El ejemplo descrito ilustra un árbol binario de orden transversal. Un árbol de búsqueda binaria es un caso especial de árbol binario en el cual los datos de cada árbol están en orden.

Recursión frente a iteración

En el ejemplo "factorial" la implementación iterativa es probablemente más rápida en la práctica que la recursiva. Esto es casi definido por la implementación del algoritmo euclidiano. Este resultado es lógico, pues las funciones iterativas no tienen que pagar el exceso de llamadas de funciones como en el caso de las funciones recursivas, y ese exceso es relativamente alto en muchos lenguajes de programación (nótese que mediante el uso de una lookup table es una implementación aún más rápida de la función factorial).

Hay otros tipos de problemas cuyas soluciones son inherentemente recursivas, porque estar al tanto del estado anterior. Un ejemplo es el árbol transversal; otros incluyen la función de Ackermann y el algoritmo divide y vencerás tales como Quicksort. Todos estos algoritmos pueden implementarse iterativamente con la ayuda de una pila, pero la necesidad del mismo, puede que anule las ventajas de la solución iterativa.

Otra posible razón para la utilización de un algoritmo iterativo en lugar de uno recursivo es el hecho de que en los lenguajes de programación modernos, el espacio de stack disponible para un hilo es, a menudo, mucho menos que el espacio disponible en el montículo, y los algoritmos recursivos suelen requerir más espacio de stack que los algoritmos iterativos. Véase, por otro lado, la sección siguiente que trata el caso especial de la recursión de cola.

Funciones de recursión de cola

Funciones de recursión de cola son funciones que finalizan con una llamada recursiva que no crea ninguna operación deferida. Por ejemplo, la función gcd (se muestra de nuevo más abajo) es recursiva de cola; sin embargo, la función factorial (que también se muestra más abajo) no es recursiva de cola porque crea operaciones diferidas que tienen que realizarse incluso después de que se complete la última llamada recursiva. Con un compilador que automáticamente optimiza llamadas recursivas de cola, una función recursiva de cola, como por ejemplo gcd, se ejecutará usando un espacio constante. Así, el proceso que genera es esencialmente iterativo y equivalente a usar estructuras de control de lenguaje imperativo como los bucles for y while.

Recusión de cola: Recursión en aumento:
//Entrada: Los enteros x e y, de forma que x >= y e y > 0 int gcd(int x, int y) {  if (y == 0)  return x;  else  return gcd(y, x % y); } 
//Entrada: n es un entero de forma que n >= 1 int fact(int n) {  if (n == 1)  return 1;  else  return n * fact(n - 1); } 

La importancia de recursión de cola es que cuando se realiza una llamada recursiva de cola, la posición de retorno de la función que llama necesita grabarse en el call stack; cuando la función recursiva retorna, continuará directamente a partir de la posición de retorno grabada previamente. Por ello, en compiladores que dan soporte a optimización de recursión de cola, este tipo de recursión ahorra espacio y tiempo.

Orden en la invocación de una función

El orden de invocación de una función puede alterar la ejecución de una función, véase este ejemplo en C:

Función 1

void recursiveFunction(int num) {  if (num < 5) {  printf("%d\n", num);  recursiveFunction(num + 1);  } } 

 

Función 2 con líneas cambiadas

void recursiveFunction(int num) {  if (num < 5) {  recursiveFunction(num + 1);  printf("%d\n", num);  } } 

 

Recursión directa e indirecta

Se habla de recursión directa cuando la función se llama a sí misma. Se habla de recursión indirecta cuando, por ejemplo, una función A llama a una función B, que a su vez llama a una función C, la cual llama a la función A. De esta forma es posible crear largas cadenas y ramificaciones, véase Parser descendente recursivo.

Véase también

Notas y referencias

  1. Epp, Susanna (1995). Discrete Mathematics with Application (2. edición). p. 427. 
  2. Graham, Ronald; Donald Knuth, Oren Patashnik (1990). Concrete Mathematics. Capítulo 1: Recurrent Problems. 
  3. Wirth, Niklaus (1976). Algorithms + Data Structures = Programs. Prentice-Hall. p. 126. 
  4. Felleisen, Matthias; Robert Bruce Findler, Matthew Flatt, Shriram Krishnamurthi (2001). How to Design Programs: An Introduction to Computing and Programming. Cambridge, MASS: MIT Press. p. art V "Generative Recursion". 
  5. Felleisen, Matthias (2002). «Developing Interactive Web Programs». En Jeuring, Johan, ed. Advanced Functional Programming: 4th International School. Oxford, Reino Unido: Springer. pp. 108. .
  6. Epp, Susanna (1995). Discrete Mathematics with Applications. Brooks-Cole Publishing Company. p. 424. 
  7. Abelson, Harold; Gerald Jay Sussman (1996). . Sección 1.2.2. Archivado desde el original el 3 de septiembre de 2006. Consultado el 13 de mayo de 2010. 
  8. Abelson, Harold; Gerald Jay Sussman (1996). . Section 1.2.5. Archivado desde el original el 3 de septiembre de 2006. Consultado el 13 de mayo de 2010. 
  9. Graham, Ronald; Donald Knuth, Oren Patashnik (1990). Concrete Mathematics. Chapter 1, Section 1.1: The Tower of Hanoi. 
  10. Epp, Susanna (1995). Discrete Mathematics with Applications (2nd edición). pp. 427-430: The Tower of Hanoi. 
  11. Epp, Susanna (1995). Discrete Mathematics with Applications (2nd edición). pp. 447-448: An Explicit Formula for the Tower of Hanoi Sequence. 
  12. Wirth, Niklaus (1976). Algorithms + Data Structures = Programs. Prentice-Hall. p. 127. 
  13. Felleisen, Matthias (2002), «Developing Interactive Web Programs», en Jeuring, Johan, ed., Advanced Functional Programming: 4th International School, Oxford, UK: Springer, p. 108 ..

Enlaces externos

  • Harold Abelson and Gerald Sussman: "Structure and Interpretation Of Computer Programs"
  • IBM DeveloperWorks: "Mastering Recursive Programming"
  • David S. Touretzky: "Common Lisp: A Gentle Introduction to Symbolic Computation"
  • Matthias Felleisen: "How To Design Programs: An Introduction to Computing and Programming"
  •   Datos: Q264164

recursión, ciencias, computación, para, tratamiento, más, general, fenómenos, recursivos, artículo, recursión, recursión, ciencias, computación, forma, atajar, solventar, problemas, hecho, recursión, ideas, centrales, ciencia, computación, resolver, problema, . Para un tratamiento mas general de los fenomenos recursivos ver el articulo de Recursion Recursion es en ciencias de la computacion una forma de atajar y solventar problemas De hecho recursion es una de las ideas centrales de ciencia de computacion 1 Resolver un problema mediante recursion significa que la solucion depende de las soluciones de pequenas instancias del mismo problema 2 El poder de la recursion evidentemente se fundamenta en la posibilidad de definir un conjunto infinito de objetos con una declaracion finita Igualmente un numero infinito de operaciones computacionales puede describirse con un programa recursivo finito incluso en el caso de que este programa no contenga repeticiones explicitas 3 La mayoria de los lenguajes de programacion dan soporte a la recursion permitiendo a una funcion llamarse a si misma desde el texto del programa Los lenguajes imperativos definen las estructuras de loops como while y for que son usadas para realizar tareas repetitivas Algunos lenguajes de programacion funcionales no definen estructuras de loops sino que posibilitan la recursion llamando codigo de forma repetitiva La teoria de la computabilidad ha demostrado que estos dos tipos de lenguajes son matematicamente equivalentes es decir que pueden resolver los mismos tipos de problemas aunque los lenguajes funcionales carezcan de las tipicas estructuras while y for Arbol basado en la recursion creado usando el lenguaje de programacion Logo Indice 1 Algoritmos recursivos 2 Programacion recursiva 2 1 Ejemplos de subrutinas definidas recursivamente recursion generativa 2 1 1 Factorial 2 1 2 Fibonacci 2 1 3 Maximo comun divisor 2 1 4 Torres de Hanoi 2 1 5 Busqueda binaria 2 2 Estructuras de datos recursivo recursion estructural 2 2 1 Listas enlazadas 2 2 2 Arboles binarios 2 3 Recursion frente a iteracion 3 Funciones de recursion de cola 4 Orden en la invocacion de una funcion 4 1 Funcion 1 4 2 Funcion 2 con lineas cambiadas 5 Recursion directa e indirecta 6 Vease tambien 7 Notas y referencias 8 Enlaces externosAlgoritmos recursivos EditarUn algoritmo recursivo es un algoritmo que expresa la solucion un problema en terminos de una llamada a si mismo La llamada a si mismo se conoce como llamada recursiva o Recursion ciencias de computacion recurrente Generalmente si la primera llamada al subprograma se plantea sobre un problema de tamano u orden N cada nueva ejecucion recurrente del mismo se planteara sobre problemas de igual naturaleza que el original pero de un tamano menor que N De esta forma al ir reduciendo progresivamente la complejidad del problema que resolver llegara un momento en que su resolucion sea mas o menos trivial o al menos suficientemente manejable como para resolverlo de forma no recursiva En esa situacion diremos que estamos ante un caso base de la recursividad Las claves para construir un subprograma recurrente son Cada llamada recurrente se deberia definir sobre un problema de menor complejidad algo mas facil de resolver Ha de existir al menos un caso base para evitar que la recurrencia sea infinita Es frecuente que los algoritmos recurrentes sean mas ineficientes en tiempo que los iterativos aunque suelen ser mucho mas breves en espacio Un metodo frecuente para simplificar es dividir un problema en problemas derivados de menor tamano del mismo tipo Esto se conoce como dialecting Como tecnica de programacion se denomina divide y venceras y es pieza fundamental para el diseno de muchos algoritmos de importancia asi como parte esencial de la programacion dinamica Virtualmente todos los lenguajes de programacion modernos permiten la especificacion directa de funciones y subrutinas recursivas Cuando se llama una funcion de este tipo el ordenador para la mayoria de los lenguajes en casi todas las arquitecturas basadas en una pila stack o en la implementacion del lenguaje lleva la cuenta de las distintas instancias de la funcion en numerosas arquitecturas mediante el uso de un call stack aunque no de forma exclusiva A la inversa toda funcion recursiva puede transformarse en una funcion iterativa usando un stack La mayoria aunque no todas de las funciones y subrutinas que pueden ser evaluadas por un ordenador pueden expresarse en terminos de una funcion recursiva sin tener que utilizar una iteracion pura a la inversa cualquier funcion recursiva puede expresarse en terminos de una iteracion pura dado que la recursion es de por si tambien iterativa Para evaluar una funcion por medio de la recursion tiene que definirse como una funcion de si misma ej el factor n n n 1 donde 0 se define como 1 Resulta evidente que no todas las evaluaciones de funciones se prestan a un acercamiento recursivo Por lo general todas las funciones finitas pueden describirse directamente de forma recursiva las funciones infinitas ej las series de e 1 1 2 2 3 3 necesitan un criterio extra para detenerse ej el numero de iteraciones o el numero de digitos significativos en caso contrario una iteracion recursiva resultaria en un bucle infinito A modo de ilustracion Si se encuentra una palabra desconocida en un libro el lector puede anotar la pagina actual en un papel y ponerlo en una pila hasta entonces vacia El lector consulta la palabra en otro articulo y de nuevo descubre otra palabra desconocida la anota y la pone en la pila y asi sucesivamente Llega un momento que el lector lee un articulo que donde todas las palabras son conocidas El lector retorna entonces a la ultima pagina y continua la lectura desde ahi y asi hasta que se retira la ultima nota de la pila retornando entonces al libro original Este modus operandi es recursivo Algunos lenguajes disenados para programacion logica y programacion funcional ofrecen la recursion como el unico medio de repeticion directa disponible para el programador Estos lenguajes suelen conseguir que la recursion de cola sea tan eficiente como la iteracion permitiendo a los programadores expresar otras estructuras repetitivas tales como map y for de scheme en terminos de recursion La recursion esta profundamente anclada en la teoria de computacion con la equivalencia teorica de funcion microrecursiva y maquinas de Turing en la cimentacion de ideas sobre la universalidad del ordenador moderno Programacion recursiva EditarCrear una subrutina recursiva requiere principalmente la definicion de un caso base y entonces definir reglas para subdividir casos mas complejos en el caso base Para una subrutina recursiva es esencial que con cada llamada recursiva el problema se reduzca de forma que al final llegue al caso base Algunos expertos clasifican la recursion como generativa o bien estructural La distincion se hace segun de donde provengan los datos con los que trabaja la subrutina Si los datos proceden de una estructura de datos similar a una lista entonces la subrutina es estructuralmente recursiva en caso contrario es generativamente recursiva 4 Muchos algoritmos populares generan una nueva cantidad de datos a partir de los datos aportados y recurren a partir de ahi HTDP How To Design Programs al espanol Como disenar programas se refiere a esta variante como recursion generativa Ejemplos de recursion generativa incluyen maximo comun divisor quicksort busqueda binaria mergesort Metodo de Newton fractals e integracion adaptiva 5 Ejemplos de subrutinas definidas recursivamente recursion generativa Editar Factorial Editar Un ejemplo clasico de una subrutina recursiva es la funcion usada para calcular el factorial de un entero Definicion de la funcion fact n s i n 0 1 s i n gt 0 n fact n 1 displaystyle operatorname fact n left begin array lccl si amp n 0 amp longrightarrow amp 1 si amp n gt 0 amp longrightarrow amp n cdot operatorname fact n 1 end array right Pseudocodigo recursivo funcion factorial input entero n de forma que n gt 0output n n 1 n 2 1 1 if n es 0 return 1 2 else return n factorial n 1 end factorialUna relacion recurrente es una ecuacion que relaciona terminos posteriores en la secuencia con terminos previos 6 Relacion recurrente de un factorial b n n b n 1 displaystyle b n nb n 1 b 0 1 displaystyle b 0 1 Computando la relacion recurrente para n 4 b4 4 b3 4 3 b2 4 3 2 b1 4 3 2 1 b0 4 3 2 1 1 4 3 2 1 4 3 2 4 6 24Esta funcion factorial tambien puede describirse sin usar recursion haciendo uso de tipicas estructuras de bucle que se encuentran en lenguajes de programacion imperativos Pseudocodigo iterativo funcion factorial es input entero n de forma que n gt 0output n n 1 n 2 1 1 crear una variable nueva llamada running total con un valor 1 2 begin loop 1 si n es 0 salir del loop 2 cambiar running total a running total n 3 decrementar n 4 repetir el loop 3 return running total end factorialEl lenguaje de programacion scheme es sin embargo un lenguaje de programacion funcional y no define estructuras de loops de cualquier tipo Se basa unicamente en la recursion para ejecutar todo tipo de loops Dado que scheme es recursivo de cola se puede definir una subrutina recursiva que implementa la subrutina factorial como un proceso iterativo es decir usa espacio constante pero tiempo lineal Fibonacci Editar Otra popular secuencia recursiva es el Numero de Fibonacci Los primeros elementos de la secuencia son 0 1 1 2 3 5 8 13 21 Definicion de la funcion fib n s i n 0 0 s i n 1 1 s i n gt 2 fib n 2 fib n 1 displaystyle operatorname fib n left begin array lccl si amp n 0 amp longrightarrow amp 0 si amp n 1 amp longrightarrow amp 1 si amp n gt 2 amp longrightarrow amp operatorname fib n 2 operatorname fib n 1 end array right Pseudocodigofunction fib is input entero n de forma que n gt 0 1 si n es 0 return 0 2 si n es 1 return 1 3 else return fib n 1 fib n 2 end fibRelacion recurrente para Fibonacci bn bn 1 bn 2 b1 1 b0 0 Computando la relacion recurrente para n 4 b4 b3 b2 b2 b1 b1 b0 b1 b0 1 1 0 1 0 1 1 0 3Este algoritmo de Fibonacci es especialmente malo pues cada vez que se ejecuta la funcion realizara dos llamadas a la funcion a si misma cada una de las cuales hara a la vez dos llamadas mas y asi sucesivamente hasta que terminen en 0 o en 1 El ejemplo se denomina recursion de arbol y sus requisitos de tiempo crecen de forma exponencial y los de espacio de forma lineal 7 Maximo comun divisor Editar Otro famosa funcion recursiva es el algoritmo de Euclides usado para computar el maximo comun divisor de dos enteros Definicion de la funcion mcd x y s i y 0 x s i y gt 0 x y mcd y m o d x y displaystyle operatorname mcd x y left begin array llcl si amp y 0 amp longrightarrow amp x si amp y gt 0 land x geq y amp longrightarrow amp operatorname mcd y mod x y end array right Pseudocodigo recursivo function gcd is input entero x entero y de forma que x gt y y y gt 0 1 if y is 0 return x 2 else return gcd y remainder of x y end gcdRelacion recursiva del maximo comun denominador donde x y displaystyle x y expresa el resto de la division entera x y displaystyle x y gcd x y gcd y x y displaystyle gcd x y gcd y x y gcd x 0 x displaystyle gcd x 0 x Computando la relacion recurrente para x 27 e y 9 gcd 27 9 gcd 9 27 9 gcd 9 0 9Computando la relacion recurrente para x 259 e y 111 gcd 259 111 gcd 111 259 111 gcd 111 37 gcd 37 0 37Notese que el algoritmo recursivo mostrado arriba es de hecho unicamente de cola recursiva lo que significa que es equivalente a un algoritmo iterativo En el ejemplo siguiente se muestra el mismo algoritmo usando explicitamente iteracion No acumula una cadena de operaciones deferred sino que su estado es mas bien mantenido completamente en las variables x e y Su number of steps grows the as the logarithm of the numbers involved 8 al espanol numero de pasos crece a medida que lo hace el logaritmo de los numeros involucrados Pseudocodigo funcion gcd es input entero x entero y de forma que x gt y e y gt 0 1 crear una nueva variable llamada remainder 2 begin loop 1 if y is zero exit loop 2 set remainder to the remainder of x y 3 set x to y 4 set y to remainder 5 repeat loop 3 return x end gcdEl algoritmo iterativo requiere una variable temporal e incluso supuesto el conocimiento del Algoritmo de Euclides es mas dificil de entender el proceso a simple vista aunque los dos algoritmos son muy similares en sus pasos Torres de Hanoi Editar Articulo principal Torres de Hanoi Para una detallada discusion de la descripcion de este problema de su historia y de su solucion consultese el articulo principal 9 10 El problema puesto de forma simple es el siguiente Dadas 3 pilas una con un conjunto de N discos de tamano creciente determina el minimo optimo numero de pasos que lleva mover todos los discos desde su posicion inicial a otra pila sin colocar un disco de mayor tamano sobre uno de menor tamano Definicion de la funcion hanoi n s i n 1 1 s i n gt 1 2 hanoi n 1 1 displaystyle operatorname hanoi n left begin array lccl si amp n 1 amp longrightarrow amp 1 si amp n gt 1 amp longrightarrow amp 2 cdot operatorname hanoi n 1 1 end array right Relacion de recurrencia para hanoi h n 2 h n 1 1 displaystyle h n 2h n 1 1 h 1 1 displaystyle h 1 1 Computacion de la relacion de recurrencia para n 4 hanoi 4 2 hanoi 3 1 2 2 hanoi 2 1 1 2 2 2 hanoi 1 1 1 1 2 2 2 1 1 1 1 2 2 3 1 1 2 7 1 15Ejemplos de implementacion Pseudocodigo recursivo function hanoi is input integer n such that n gt 1 1 if n is 1 then return 1 2 return 2 call hanoi n 1 1 end hanoiAunque no todas las funciones recursivas tienen una solucion explicita la secuencia de la Torre de Hanoi puede reducirse a una formula explicita 11 Una formula explicita de las Torres de Hanoi h1 1 21 1 h2 3 22 1 h3 7 23 1 h4 15 24 1 h5 31 25 1 h6 63 26 1 h7 127 27 1 Por lo general hn 2n 1 for all n gt 1Busqueda binaria Editar El algoritmo de busqueda binaria es un metodo de busqueda de un dato en un vector de datos ordenado dividiendo el vector en dos tras cada pasada El truco es escoger un punto cerca del centro del vector comparar en ese punto el dato con el dato buscado para responder entonces a una de las siguientes 3 condiciones se encuentra el dato buscado el dato en el punto medio es mayor que el valor buscado o el dato en el punto medio es menor que el valor buscado Se usa recursion en este algoritmo porque tras cada pasada se crea un nuevo vector dividiendo en original en dos La subrutina de busqueda binaria se llama entonces de forma recursiva cada vez con un vector de menor tamano El tamano del vector se ajusta normalmente cambiando el indice inicial y final El algoritmo muestra un orden logaritmo de crecimiento porque divide esencialmente el dominio del problema en dos tras cada pasada Ejemplo de implementacion de la busqueda binaria Call binary search with proper initial conditions Entrada Los datos se presentan en forma de vector de numero entero numeros enteros ordenado de forma ascendente toFind es el numero entero a buscar count es el numero total de elementos del vector Salida resultado de la busqueda binaria int search int data int toFind int count Start 0 indice inicial End count 1 indice superior return binary search data toFind 0 count 1 Algoritmo de la busqueda binaria Entrada Los datos se presentan en forma de vector de numero entero numeros enteros ordenado de forma ascendente toFind es el numero entero a buscar start es el indice minimo del vector end es el indice maximo del vector Salida posicion del numero entero toFind dentro del vector de datos 1 en caso de busqueda fallida int binary search int data int toFind int start int end Averigua el punto medio int mid start end start 2 Division de enteros Condicion para detenerse if start gt end return 1 else if data mid toFind Encontrado return mid else if data mid gt toFind El dato es mayor que toFind se busca en la mitad inferior return binary search data toFind start mid 1 else El dato es menor que toFind se busca en la mitad superior return binary search data toFind mid 1 end Estructuras de datos recursivo recursion estructural Editar Una aplicacion de importancia de la recursion en ciencias de la computacion es la definicion de estructuras de datos dinamicos tales como listas y arboles Las estructuras de datos recursivos pueden crecer de forma dinamica hasta un tamano teorico infinito en respuesta a requisitos del tiempo de ejecucion por su parte los requisitos del tamano de un vector estatico deben declararse en el tiempo de complicacion Los algoritmos recursivos son especialmente apropiados cuando el problema que resolver o los datos que manejar son definidos en terminos recursivos 12 Los ejemplos en esta seccion ilustran lo que se conoce como recursion estructural Este termino se refiere al hecho de que las subrutinas recursivas se aplican a datos que se definen de forma recursiva En la medida en que un programador deriva una plantilla de una definicion de datos las funciones emplean recursion estructural Es decir las recursiones en el cuerpo de una funcion consumen una determinada cantidad de un compuesto dado de forma inmediata 13 Listas enlazadas Editar A continuacion se describe una definicion simple del nodo de una lista enlazada Notese como se define el nodo por si solo El siguiente elemento del nodo del struct es un puntero a un nodo de struct struct node int n algun tipo de datos struct node next puntero a otro nodo de struct LIST no es otra cosa que un nodo de struct typedef struct node LIST Las subrutinas que operan en la estructura de datos de LIST pueden implementarse de forma natural como una subrutina recursiva porque la estructura de datos sobre la que opera LIST es definida de forma recursiva La subrutina printList definida a continuacion recorre la lista hacia abajo hasta que esta se vacia NULL para cada nodo imprime el dato un numero entero En la implementacion en C la lista permanece inalterada por la subrutina printList void printList LIST lst if isEmpty lst caso basico printf d lst gt n imprime el entero seguido por un espacio printList lst gt next llamada recursiva Arboles binarios Editar Mas abajo se muestra una definicion simple de un nodo de arbol binario Al igual que el nodo de listas enlazadas se define a si misma de forma recursiva Hay dos punteros que se refieren a si mismos left apuntando a l aparte izquierda del subarbol y right a la parte derecha del subarbol struct node int n algun tipo de datos struct node left puntero al subarbol izquierdo struct node right puntero al subarbol derecho TREE no es otra cosa que un nodo struct typedef struct node TREE Las operaciones en el arbol pueden implementarse usando recursion Notese que debido al hecho de que hay dos punteros que se referencian a si mismos izquierda y derecha esas operaciones del arbol van a necesitar dos llamadas recursivas Para un ejemplo similar vease la funcion de Fibonacci y la explicacion siguiente void printTree TREE t if isEmpty t caso basico printTree t gt left ir a la izquierda printf d t gt n imprimir el entero seguido de un espacio printTree t gt right ir a la derecha El ejemplo descrito ilustra un arbol binario de orden transversal Un arbol de busqueda binaria es un caso especial de arbol binario en el cual los datos de cada arbol estan en orden Recursion frente a iteracion Editar En el ejemplo factorial la implementacion iterativa es probablemente mas rapida en la practica que la recursiva Esto es casi definido por la implementacion del algoritmo euclidiano Este resultado es logico pues las funciones iterativas no tienen que pagar el exceso de llamadas de funciones como en el caso de las funciones recursivas y ese exceso es relativamente alto en muchos lenguajes de programacion notese que mediante el uso de una lookup table es una implementacion aun mas rapida de la funcion factorial Hay otros tipos de problemas cuyas soluciones son inherentemente recursivas porque estar al tanto del estado anterior Un ejemplo es el arbol transversal otros incluyen la funcion de Ackermann y el algoritmo divide y venceras tales como Quicksort Todos estos algoritmos pueden implementarse iterativamente con la ayuda de una pila pero la necesidad del mismo puede que anule las ventajas de la solucion iterativa Otra posible razon para la utilizacion de un algoritmo iterativo en lugar de uno recursivo es el hecho de que en los lenguajes de programacion modernos el espacio de stack disponible para un hilo es a menudo mucho menos que el espacio disponible en el monticulo y los algoritmos recursivos suelen requerir mas espacio de stack que los algoritmos iterativos Vease por otro lado la seccion siguiente que trata el caso especial de la recursion de cola Funciones de recursion de cola EditarFunciones de recursion de cola son funciones que finalizan con una llamada recursiva que no crea ninguna operacion deferida Por ejemplo la funcion gcd se muestra de nuevo mas abajo es recursiva de cola sin embargo la funcion factorial que tambien se muestra mas abajo no es recursiva de cola porque crea operaciones diferidas que tienen que realizarse incluso despues de que se complete la ultima llamada recursiva Con un compilador que automaticamente optimiza llamadas recursivas de cola una funcion recursiva de cola como por ejemplo gcd se ejecutara usando un espacio constante Asi el proceso que genera es esencialmente iterativo y equivalente a usar estructuras de control de lenguaje imperativo como los bucles for y while Recusion de cola Recursion en aumento Entrada Los enteros x e y de forma que x gt y e y gt 0 int gcd int x int y if y 0 return x else return gcd y x y Entrada n es un entero de forma que n gt 1 int fact int n if n 1 return 1 else return n fact n 1 La importancia de recursion de cola es que cuando se realiza una llamada recursiva de cola la posicion de retorno de la funcion que llama necesita grabarse en el call stack cuando la funcion recursiva retorna continuara directamente a partir de la posicion de retorno grabada previamente Por ello en compiladores que dan soporte a optimizacion de recursion de cola este tipo de recursion ahorra espacio y tiempo Orden en la invocacion de una funcion EditarEl orden de invocacion de una funcion puede alterar la ejecucion de una funcion vease este ejemplo en C Funcion 1 Editar void recursiveFunction int num if num lt 5 printf d n num recursiveFunction num 1 Funcion 2 con lineas cambiadas Editar void recursiveFunction int num if num lt 5 recursiveFunction num 1 printf d n num Recursion directa e indirecta EditarSe habla de recursion directa cuando la funcion se llama a si misma Se habla de recursion indirecta cuando por ejemplo una funcion A llama a una funcion B que a su vez llama a una funcion C la cual llama a la funcion A De esta forma es posible crear largas cadenas y ramificaciones vease Parser descendente recursivo Vease tambien EditarRecursion primitiva Programacion funcional Funcion de AckermannNotas y referencias EditarEsta obra contiene una traduccion derivada de Recursion computer science de Wikipedia en ingles concretamente de esta version publicada por sus editores bajo la Licencia de documentacion libre de GNU y la Licencia Creative Commons Atribucion CompartirIgual 3 0 Unported Epp Susanna 1995 Discrete Mathematics with Application 2 edicion p 427 Graham Ronald Donald Knuth Oren Patashnik 1990 Concrete Mathematics Capitulo 1 Recurrent Problems La referencia utiliza el parametro obsoleto coautores ayuda Wirth Niklaus 1976 Algorithms Data Structures Programs Prentice Hall p 126 Felleisen Matthias Robert Bruce Findler Matthew Flatt Shriram Krishnamurthi 2001 How to Design Programs An Introduction to Computing and Programming Cambridge MASS MIT Press p art V Generative Recursion La referencia utiliza el parametro obsoleto coautores ayuda Felleisen Matthias 2002 Developing Interactive Web Programs En Jeuring Johan ed Advanced Functional Programming 4th International School Oxford Reino Unido Springer pp 108 Epp Susanna 1995 Discrete Mathematics with Applications Brooks Cole Publishing Company p 424 Abelson Harold Gerald Jay Sussman 1996 Structure and Interpretation of Computer Programs Seccion 1 2 2 Archivado desde el original el 3 de septiembre de 2006 Consultado el 13 de mayo de 2010 Abelson Harold Gerald Jay Sussman 1996 Structure and Interpretation of Computer Programs Section 1 2 5 Archivado desde el original el 3 de septiembre de 2006 Consultado el 13 de mayo de 2010 Graham Ronald Donald Knuth Oren Patashnik 1990 Concrete Mathematics Chapter 1 Section 1 1 The Tower of Hanoi La referencia utiliza el parametro obsoleto coautores ayuda Epp Susanna 1995 Discrete Mathematics with Applications 2nd edicion pp 427 430 The Tower of Hanoi Epp Susanna 1995 Discrete Mathematics with Applications 2nd edicion pp 447 448 An Explicit Formula for the Tower of Hanoi Sequence Wirth Niklaus 1976 Algorithms Data Structures Programs Prentice Hall p 127 Felleisen Matthias 2002 Developing Interactive Web Programs en Jeuring Johan ed Advanced Functional Programming 4th International School Oxford UK Springer p 108 Enlaces externos EditarHarold Abelson and Gerald Sussman Structure and Interpretation Of Computer Programs IBM DeveloperWorks Mastering Recursive Programming David S Touretzky Common Lisp A Gentle Introduction to Symbolic Computation Matthias Felleisen How To Design Programs An Introduction to Computing and Programming Datos Q264164 Obtenido de https es wikipedia org w index php title Recursion ciencias de computacion amp oldid 139480211, wikipedia, wiki, leyendo, leer, libro, biblioteca,

español

, española, descargar, gratis, descargar gratis, mp3, video, mp4, 3gp, jpg, jpeg, gif, png, imagen, música, canción, película, libro, juego, juegos