Recursividad

“Para entender la recursividad, primero hay que entender la recursividad”

Motivación

La recursividad es una herramienta poderosa que permite solucionar problemas bastante complejos de forma simple y eficiente. Una definición bastante simplista es que un método recursivo es aquel método que se invoca a sí mismo. Considera el siguiente ejemplo:

  1. int NumeroDigitos(int n)

  2. {

  3. if (n <= 9)

  4. return 1;

  5. else

  6. return 1+NumeroDigitos(n/10);

  7. }

Acá el método NumeroDigitos se invoca a sí mismo en la línea 6.

El programa iterativo equivalente es

  1. int NumeroDigitos (int n)

  2. {

  3. int cifras=1;

  4. while (n>9)

  5.     {

  6.     cifras=cifras+1;

  7.     n=n/10;

  8.     }

  9. return cifras;

  10. }

¿Qué hace el computador con el siguiente llamado?

int x=NúmeroDigitos(832);



1) Llamar al método NúmeroDigitos con el argumento 832. Como n es mayor que 10...

2) ...el método se invoca a sí mismo con el argumento 83 (n/10)

3) Como n(83) es mayor que 10, el método vuelve a llamarse con el valor 8 (n/10)

4) n es menor que diez, por lo que el método que fue llamado con el valor 8 devuelve 1

5) El método que fue llamado con 83 devuelve 1+1, es decir, 2

6) El método devuelve 2+1=3

7) en x se almacena 3

El siguiente diagrama muestra lo anterior:




Seguir la ejecución suele ser bastante dificil en casos más complejos. Una forma más simple de abordar el funcionamiento de este método es definir el problema de encontrar la cantidad de dígitos de un número en función de sí mismo y a partir de un caso básico:

  1. La cantidad de cifras de un número menor que 10 es 1 (caso básico) (líneas 2-3)

  2. La cantidad de cifras de un número con más de 1 cifra es la cantidad de cifras del número dividido en 10 más uno (línea 5)

Si te fijas, en la segunda parte de la definición, aparece destacado con negritas la definición recursiva, esa definición se conoce como invariante. Una vez que uno ha encontrado una definición recursiva para un problema, suele ser fácil programarla.

Considera el problema de escribir en base binaria un número n. Los números en base 2 usan sólo dos dígitos, 0 y 1. Tienen algunas propiedades interesantes. Una de ellas es que la división entera de un número binario por dos se puede llevar a cabo borrando la última cifra (análogamente a la división por 10 de un número en base 10):

17/2=10001/2=8=1000

31/2=11111/2=15=1111

37/2=100101/2=18=10010



Lo que lleva a que la notación binaria de un número n es la notación binaria de n/2 seguida de un 1 si n es impar o 0 si n es par (*). Por ejemplo,

27 es 11011, que es la notación de 13 (1101) seguido de 1, pues 27 es impar.

13 es 1101, que es la notación de 6 (110) seguido de 1, pues 13 es impar

6 es 110, que es 3 (11) seguido de 0, pues 6 es par

3 es 11, que es 1 seguido de 1, pues 3 es impar



La definición anterior (*) es recursiva!:La notación binaria de un número n es la notación binaria de n/2 seguida de un 1 si n es impar o 0 si n es par (*).

El caso básico es cuando n es 1 o 0. En ese caso, la notación es la misma en binario o decimal. El programa recursivo queda:

  1. void EscribirEnBinario(int n)

  2. {

  3. if (n<=1)

  4.     printf (“%d”,n);

  5. else

  6.     {

  7.     EscribirEnBinario (n/2);

  8.     print (“%d”, n % 2);

  9.     }

  10. }



La potencia de un número real también puede definirse recursivamente:

xn es

            1 si n = 0

            x·xn-1 si n>0

            1/(xn) si n<0



La función es trivial:

  1. float potencia (double x, int n)

  2. {

  3. if (n==0)

  4.     {

  5.     return 1;

  6.     }

  7. else

  8.     {

  9.     if (n<0)

  10.         {

  11.         return 1/potencia(x,abs(n));

  12.         }

  13.    else

  14.         {

  15.         return x*potencia(x,n-1);

  16.         };

  17.    };

  18. }

Nota que si la cantidad de veces que se llama el método a sí mismo es (n-1). Existe otra definición recursiva que nos permitirá efectuar sólo log2(n) llamados:

xn es

            1 si n = 0

            1/(xn) si n<0

            xn/2·xn/2 si n es par (/ es división entera)

            x·x(n-1)/2 ·x(n-1)/2 si n es impar



  1. float potencia (double x, int n)

  2. {

  3. if (n==0)

  4.     {

  5.     return 1;

  6.     }

  7. else

  8.     {

  9.     if (n<0)

  10.         {

  11.         return 1/potencia(x,abs(n));

  12.         }

  13.    else

  14.         {

  15.         if (n%2==1) /* IMPAR */

  16.              {

  17.              double p=potencia(x, (n-1) div 2);

  18.              return x*p*p;

  19.              }

  20.         else         /* PAR */

  21.              {

  22.              double p=potencia(x, n div 2);

  23.              return p*p;

  24.              }

  25.         }

  26.     }

  27. }

La recursividad también se puede aplicar sobre arreglos. Por ejemplo, si quieres calcular la suma de todos los elementos de un arreglo puedes usar la siguiente definición recursiva:

Si A no tiene elementos, su suma es 0

Suma Ai(i=1..n) = Suma Ai (i=1..n-1) + An



  1. int suma (int A[], int n)

  2. {

  3. if (n==0)

  4.     return 0;

  5. else

  6.     return a[n-1]+suma(a,n-1);

  7. }

¿Y el menor valor de un arreglo? Es fácil darse una definición recursiva:

  1. int menor (int A[], int n)

  2. {

  3. if (n==1)

  4.     return=A[0];

  5. else

  6.     return min(A[n-1],menor (A, n-1));

  7. }



Problemas Resueltos

Pregunta 1

La Sucesión de Fibonacci es una secuencia de números naturales, que empieza con 0 y 1, y cuyos siguientes términos están definidos como la suma de los dos anteriores:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ...

Luego podemos definir la función de Fibonacci de manera recursiva:

Fibonacci(n):

A partir de esta definición se pide una función recursiva que genere el n-esimo término de la sucesión. El número n se debe pedir al usuario vía teclado.   (Típico ejemplo de recursividad con dos casos base).

Solución

int fib(int n)

{

if (n ==0)

    {

    return 0;

    }

else if (n==1)

    {

    return 1;

    }

else

    {

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

    }

                        

}



Pregunta 2

Escriba un programa que sirva para saber las posibilidades de movimiento de un caballo en una partida de ajedréz de una casilla a otra (por ejemplo para comer una pieza). Para ello se ha creado una matriz A[8][8] la cual contiene un mapa del tablero (0 si la casilla está vacía o 1 si está ocupada) y se debe crear un programa donde ingresando el punto de inicio y el de destino diga si existe una combinación de movimientos posibles que lo lleven del inicio al destino sin que pase a través de una casilla ocupada o no.   (Problema difícil!!!!, estudienlo)

Solución

/* 1 es SI HAY, 0 es NO HAY */



int recorrer(int A[][], int i, int j, int p, int q)

{

if (i<0 || i>=8 || j<0 || j>=8) return false; /* Fuera del tablero */

if ((i==p) && (j==q)) return true;            /* Mismo lugar */

if (A[i][j]==1) return false;                 /* casilla ocupada */

A[i][j]=1;                                    /* Ocupo la casilla */

                                             /* Así evito ciclos */

return recorrer (A, i-2, j-1, p,q) ||

       recorrer (A, i-2, j+1, p,q) ||

       recorrer (A, i-1, j-2, p,q) ||

       recorrer (A, i-1, j+2, p,q) ||

       recorrer (A, i+1, j-2, p,q) ||

       recorrer (A, i+1, j+2, p,q) ||

       recorrer (A, i+2, j-1, p,q) ||

       recorrer (A, i+2, j+1, p,q);    

}



Pregunta 3

Un copo de nieve se puede definir como un punto, varias ramas en círculo que salen de él y luego un copo de nieve de 1/3 del tamaño anterior en la punta de cada una de esas ramas. Las ramas son del mismo tamaño y están dispuestas con el mismo ángulo entre ellas. Se pide escribir una función recursiva:

dibuja_arbol(int x, int y, int n, int l, int nivel)

Donde (x,y) son las coordenadas del centro; n es el número de ramas; l es el largo de las ramas y nivel es el número de niveles que quiero (en el último nivel se dibuja un círculo de radio l). Por ejemplo, del llamado dibuja_arbol(400,300,6,200,4) se obtiene:






Para calcular las ramas, deben encontrar el ángulo alfa y las coordenadas de las puntas de todas las ramas, usando seno y coseno del ángulo correspondiente. Para el caso de 8 ramas se tiene:


Para dibujar posee las funciones linea (int x1, int y1, int x2, int y2) que dibuja una línea entre los puntos (x1,y1) y (x2,y2). Y el método circ (int x, int y, int r) que dibuja una circunferencia de centro (x,y) y radio r.

Solución

#include <math.h>



void dibuja_arbol(int x, int y, int n, int l, int nivel)

{

float alfa = 2*PI/n;



if(nivel == 0)

    {

    circ(x,y,l);

    return;

    }



/* Aca se itera con el angulo alfa */



for(int i = 0; i < n; i++)

    {

    linea(x, y, (int)(x+l*cos(i*alfa)), (int)(y-l*sin(i*alfa)));

    dibuja_arbol((int)(x+l*cos(i*alfa)),(int)(y-l*Math.(i*alfa)),

                 n, l/3, nivel-1);

    }

}



Problemas propuestos

Problema 1: Las torres de Hanoi

Se tienen las torres de Hanoi, en que hay que llevar las n fichas de A hasta C, siempre dejando una ficha mas pequeña sobre otra mas grande. Se puede utilizar B para pasar algunas fichas. Plantee el algoritmo recursivo que lo realiza.




Problema 2: Tratamiento de enteros

Escriba una función recursiva que reciba un entero y retorne un String con los digitos invertidos y separados por *. Por ejemplo:

Ingrese un número: 12345

5 * 4 * 3 * 2 * 1