Descargar

La recursión

Enviado por Pablo Turmero


    La recursión es una técnica de programación asociada a la solución de problemas matemáticos, los cuales se definen de manera recurrente. Es decir, en términos de problemas más pequeños. Si Pn es un problema de tamaño n y F es una función, representamos una función recursiva con la siguiente fórmula

    Pn = F(Pn-1, Pn-2, Po) donde Pi es un Problema de tamaño i

    Veamos un ejemplo.

    Ejemplo 1: Factorial

    Su definición matemática seria

    N! = (N*(N-1)*(N-2)* … 1) para N>0

    0! = 1

    Si observamos el comportamiento de esta función:

    1! = 1

    2! = 2 * 1 = 2 * 1!

    3! = 3*2*1 = 3 * 2!

    N! = N*(N-1)!

    Para cada N tenemos una formula recurrente: N! = N*(N-1)!

    Si ahora queremos definir un algoritmo que acepta un entero N y retorna el valor del factorial de N (N!), podemos hacerlo de manera iterativa o recursiva.

    Si intentamos programar esta función de manera iterativa, usando el lenguaje C, con las herramientas que disponemos hasta ahora, obtendríamos una función como la siguiente:

    int Factorial (int A)

    {

    int prod = 1,i;

    for (i = n; i=0; i–)

    {

    prod * = i ;

    }

    return (prod);

    }

    Este es un algoritmo ITERATIVO porque requiere la repetición explicita de cierto proceso (multiplicar por el anterior) hasta alcanzar determinada condición.

    Si observamos la formula N! podemos notar que para cada N, esta depende del factorial calculado previamente, como habíamos notado anteriormente:

    0! = 1

    1! = 1*0!

    2! = 2*1 = 2*1!

    3! = 3*2*1 = 3*2!

    4! = 4*3*2*1 = 4*3!

    N! = N* (N-1)!

    Podríamos reescribir la definición de la función matemáticamente como:

    N! = N*(N-1)! para N > 0

    N! = 1 para N = 0

    Por lo cual la fórmula recurrente quedaría:

    Factorial(N)= Factorial(N-1) * N para N > 0

    Factorial(0)= 1 para N = 0

    En esta fórmula observamos que Factorial está definida en términos de si misma, pero en un problema más pequeño. Escribiendo el algoritmo recursivo en lenguaje C, basándonos en la fórmula recurrente:

    int factorial(int n)

    {

    if (n == 0)

    return(1);

    else

    return(n * factorial(n-1));

    }

    Note que la función se llama a si misma, a esto se le conoce como invocación recursiva. Este es el ingrediente principal de una función recursiva. Se supone que la función calculada ya ha sido escrita y se usa en la propia definición. Es importante puntualizar que la invocación recursiva debe hacerse con cuidado porque puede producirse un ciclo infinito de llamadas. Por lo tanto, en toda función recursiva debe existir una condición de parada. En el ejemplo, la condición de parada se alcanza en n=0.

    ¿Cómo sería la ejecución de una función recursiva?

    ¿Afecta el pasaje de parámetros?

    Para ilustrar esto efectuaremos la corrida de la función factorial para n=4, de forma recursiva. Llevaremos acabo este pequeño ejemplo paso por paso, para así llegar a entender de manera clara la forma en que se ejecutará una función recursiva.

    Sabemos de antemano que factorial(4) debe dar como resultado 12.

    Veamos lo que ocurre en la pila de invocación que se muestra en la siguiente tabla:

    edu.red

    Para cada nueva llamada de factorial. Se crea un nuevo registro de activación dentro de la pila de invocación donde el parámetro N cambia al valor indicado en la invocación. A si van a existir varias generaciones de parámetros simultáneamente, una en cada registro. Sólo se hace referencia a la copia mas reciente, es decir, a la copia de la generación actual.

    Note que en este caso usamos más memoria que el algoritmo iterativo. Lo cual, para este ejemplo, nos dice que es mejor el iterativo en cuanto a recursos. En cuanto a legibilidad, ambos son buenos.

    Ejemplo 2 : Números de Fibonacci

    La serie de Fibonacci está formada por los números 0,1,1,2,3,5,8,13,21,34……………

    Esta serie se construye obteniendo cada elemento como la suma de los anteriores. Es decir,

    0,1,0+1,1+1,1+2,2+3,3+5,5+8,8+13,13+21,…

    Debemos tomar dos números iniciales, que corresponden a las dos primeras posiciones de la seria.

    Asi, suponemos que

    Fib (0) = 0

    Fib (1) = 1

    Fib (2) = Fib (1) + Fib (0)

    Fib (3) = Fib (2) + Fib (1)

    Fib (n) = Fib (n-2) + Fib (n-1) n ( 2

    Por lo que la definición matemática sería:

    edu.red

    Note que esta definición hace referencia a si misma dos veces. En este caso decimos que tenemos recursión tipo árbol. Cuando hay una sola llamada recursiva se dice que la recursión es por la cola.

    La implementación de la definición recursiva de los números de fibonacci en lenguaje C quedaría:

    int Fib (int n)

    {

    if ( n <= 1 )

    {

    return(n);

    }

    else

    return(Fib(n-2) + Fib(n-1));

    }

    En esta implementación, los casos bases de la recursión fib(0)=0 y fib(1)=1 son cubiertos por la condición de parada n = 1.

    La corrida de esta función es mucho más complicada porque tenemos dos invocaciones recursivas que deben resolverse en el segundo return. Veamos que ocurre para n=4, donde el resultado debe ser 3:

    edu.red

    El árbol de invocación para Fib (4) sería:

    edu.red

    Note que hay muchas llamadas redundantes, seria mucho más eficiente recordar el valor. Se podría hacer el algoritmo iterativo pero es antinatural, ya que la definición de la función es naturalmente recursiva. Escriba un algoritmo iterativo como ejercicio.

    En este caso en cuanto a recursos es mucho mejor el iterativo, pero en cuanto a legibilidad es mejor el recursivo.

    Hasta aquí parece que la recursión es algo formal que no me ayuda en la computación de muchas actividades. Veamos un ejemplo donde la recursión mejora significativamente el tiempo de corrida de un algoritmo.

    Ejemplo 3: Búsqueda Binaria

    Consideremos un arreglo de elementos donde los objetos se colocan en orden ascendente.

    -25

    -17

    -13

    -9

    -7

    -3

    0

    4

    10

    26

    39

    45

    49

    54

    63

    70

    Algunos ejemplos de la vida real con elementos ordenados son: un diccionario, un directorio telefónico, una lista de estudiantes, una lista de carros robados ordenados por placa, etc.

    Queremos encontrar un elemento dentro del arreglo. En la vida real, una palabra en el diccionario, la dirección de una persona, la nota de un estudiantes, un carro robado.

    La búsqueda es una actividad muy común en computación. Debería ser eficiente. Nosotros conocemos el algoritmo de búsqueda secuencial o lineal, es decir, buscar desde el primer elemento hasta encontrarlo. Este algoritmo es aplicable cuando los elementos están en desorden. En el caso de elemento ordenado, no tiene sentido aplicar este algoritmo. Naturalmente al buscar en un diccionario, no se busca desde el principio sino que se intenta buscar en una de las dos mitades, y asi sucesivamente hasta encontrar la palabra. Al algoritmo que se uso de manera natural al buscar en un diccionario se le conoce como algoritmo de búsqueda binaria y es el que explicaremos a continuación.

    Veamos la representación gráfica del proceso. Se pregunta por el elemento del medio (a), si este no es el buscado ahora se toma la mitad donde se encuentra el elemento. Es decir, preguntamos si el elemento es menor que a, en cuyo caso buscamos en la primera mitad. Nuevamente se obtiene el elemento del medio (b), en el sub arreglo considerado. Y asi sucesimente se aplica el algoritmo hasta encontrar el elemento buscado o llegar a una casilla donde podemos decidir que el elemento no se encuentra.

    edu.red

    Observe que la búsqueda binaria es un método recursivo. Pues se aplica el mismo proceso sobre el nuevo sub-arrego. Para determinar el sub-arreglo debemos saber cuáles son los límites inferior (linf) y superior (lsup) dentro del arreglo original, es decir, el índice donde comienza el sub-arreglo y el índice donde finaliza el mismo. Veamos el algoritmo:

    edu.red

    En este caso los cálculos no serian redundantes pues la recursión solo recorre una rama del árbol.

    Ejercicio: Realice la corrida para el arreglo dado como ejemplo y los valores de x=10 y x=15, observe lo que ocurre.

    Características de la Recursión

    La Recursión posee ciertas características que deben cumplirse para que la misma llegue a desarrollarse adecuadamente de lo contrario podría generar gran cantidad de problemas en el programa.

    • Debe tener una condición de parada.

    • Es un buen estilo de programación colocar la(s) invocación(es) recursiva(s) al final de la función.

    • En general es menos eficiente en tiempo y espacio, asi que debe considerar esto a lo hora de elegir un algoritmo recursivo.

    • En ocasiones es la solución más natural y lógica a un problema.

    • Compromiso entre la eficiencia de la máquina y la eficiencia del programador (costo del programa aumenta mas rápido que costo de computación).

    Ejemplo 4: Torres de Hanoi

    edu.red

    Figura 3.1 . Torres de Hanoi

    El problema consiste en mover N discos de un eje (A) a otro (C), cumpliendo ciertas reglas:

    • Un disco de tamaño mayor no se puede colocar sobre un disco más pequeño.

    • Sólo se puede mover un disco a la vez.

    "Pasar los 4 discos desde el eje A hasta el eje C usando B como auxiliar" es el problema que queremos resolver representado en la figura.

    Al tratar de definirlo en términos de un problema más pequeño, la definición recursiva quedaría

    Mover la torre de Hanoi de N-1 disco desde el eje A hasta el eje B

    "Mover el disco N (más grande) desde el eje A hasta el eje C"

    Mover la torre de Hanoi de N-1 disco desde el eje B hasta el eje C

    La solución trivial (caso base) es con N = 1.

    "Mover el disco 1desde el eje A hasta el eje C."

    Note que la instrucción "Mover la torre de Hanoi de N-1 disco desde el eje A hasta el eje B" sería una invocación recursiva

    edu.red

    Figura 3.2. Ejercicio torres de Hanoi

     

     

    Autor:

    Pablo Turmero