La descarga está en progreso. Por favor, espere

La descarga está en progreso. Por favor, espere

Capítulo 17: Recursividad.

Presentaciones similares


Presentación del tema: "Capítulo 17: Recursividad."— Transcripción de la presentación:

1 Capítulo 17: Recursividad

2 En este capítulo se tratarán los siguientes temas:
6.1 Introducción 6.2 Conceptos iniciales 6.3 Otros ejemplos de recursividad 6.4 Permutar los caracteres de una cadena 6.5 Búsqueda binaria 6.6 Ordenamiento por selección 6.7 La función de Fibonacci

3 6.1 Introducción Hasta aquí hemos desarrollado algoritmos implementados como funciones que se invocan unas a otras para dividir la tarea. Como estudiamos al inicio del libro, una tarea difícil puede dividirse en varias tareas más simples, cada una de las cuales puede, a su vez, dividirse en tareas más simples todavía hasta llegar a un nivel de simplicidad en el que ya no se justifique la necesidad de volver a dividir. Sin embargo, la naturaleza de cierto tipo problemas nos inducirá a pensar soluciones basadas en funciones que se llamen a sí mismas para resolverlos. Esto no quiere decir que no puedan resolverse de “manera tradicional”; solo que, dada su naturaleza, será mucho más fácil encontrar y programar una solución recursiva que una solución iterativa tradicional. Cuando una función se invoca a sí misma decimos que es una función recursiva.

4 6.2 Conceptos iniciales 6.2.1 Funciones recursivas
Una definición es recursiva cuando “define en función de sí misma”. Análogamente, diremos que una función es recursiva cuando, para resolver un problema, se invoca a sí misma una y otra vez hasta que el problema queda resuelto. En matemática encontramos varios casos de funciones recursivas. El caso típico para estudiar este tema es el de la función factorial, que se define de la siguiente manera: Sea x perteneciente al conjunto de los números naturales (incluyendo al cero), entonces:  factorial(x) = x * factorial (x-1), para todo x > 0 factorial(x) = 1 , si x=0 La definición de la función factorial recurre a sí misma para expresar lo que necesita definir por lo tanto se trata de una definición recursiva. Ahora apliquemos la definición de la función para calcular el factorial de 5. Para interpretar la tabla que veremos a continuación debemos leer la columna de la izquierda desde arriba hacia abajo y luego la columna de la derecha desde abajo hacia arriba. Como vemos, factorial de 5 se resuelve invocando a factorial de 4, pero factorial de 4 se resuelve invocando a factorial de 3, que se resuelve invocando a factorial de 2. Este se resuelve invocando a factorial de 1, que se resuelve invocando a factorial de 0 que, por definición, es 1. Luego de obtener este valor concreto podemos reemplazar todas las invocaciones que quedaron pendientes. Esto lo hacemos en la columna de la derecha, la cual, como ya dijimos, debe leerse de abajo hacia arriba. factorial(5) = 5*factorial(4) = factorial(4) = 4*factorial(3) = factorial(3) = 3*factorial(2) = factorial(2) = 2*factorial(1) = factorial(1) = 1*factorial(0) = factorial(0) = 1; factorial(5) = 5* = 120; factorial(4) = 4* = 24; factorial(3) = 3* = 6; factorial(2) = 2* = 2; factorial(1) = 1* = 1;

5 6.2.1 Funciones recursivas El desarrollo de una función recursiva como factorial resulta extremadamente fácil ya que solo tenemos que ajustarnos a su definición matemática. La función recibe el parámetro x. Si x es igual a 0 entonces retorna 1; en cambio, si x es mayor que cero entonces retorna el producto de x, por lo que “retorna la misma función” al ser invocada con el argumento x-1.

6 6.2.2 Finalización de la recursión
Todo algoritmo recursivo debe finalizar en algún momento, de lo contrario el programa hará que se desborde la pila de llamadas y finalizará abruptamente. En el caso de la función factorial la finalización de la recursión está dada por el caso particular de x=0. En este caso la función no necesita llamarse a sí misma ya que, por definición, el factorial de 0 es 1. 6.2.3 Invocación a funciones recursivas Desde el punto de vista del programa, la función recursiva es una función común y corriente que puede invocarse como se invoca a cualquier otra función. A continuación, veremos un programa que calcula y muestra el factorial n, siendo n un valor ingresado por el usuario.

7 6.2.4 Funcionamiento de la pila de llamadas (stack)
Para entender la idea de “invocación recursiva” es muy importante comprender el funcionamiento del stack o “pila de llamadas”, que es la sección de memoria donde las funciones almacenan los valores de sus variables locales y parámetros mientras dura su tiempo de ejecución.    package mod4.cap01; public class MuestraFactorialDe4 { public static void main(String[] args) // invocamos al método factorial long f = factorial(4); // mostramos el resultado System.out.println("El factorial de 4 es: "+f); } Encontrará los ejemplos relacionados a este tema en la página 523 del libro.

8 6.2.5 Funciones recursivas vs. funciones iterativas
En general, para todo algoritmo recursivo podemos encontrar un algoritmo iterativo equivalente que resuelva el mismo problema sin tener que invocarse a sí mismo. A continuación, compararemos las implementaciones recursiva e iterativa de la función factorial. Generalmente, cuando tenemos un problema de naturaleza recursiva resulta mucho más fácil hallar una la solución que también lo sea. Sin embargo, esta solución no siempre será la más eficiente. Para probarlo, antes de finalizar este capítulo analizaremos la función de Fibonacci que, si bien se trata de un caso particular y extremo, es un excelente ejemplo que demuestra cuán ineficiente puede llegar a resultar una implementación recursiva.

9 6.3 Otros ejemplos de recursividad
Problema 1.1 – Mostrar los primeros números naturales Mostrar por pantalla los primeros n números naturales, siendo n un valor ingresado por el usuario. package mod4.cap01; import java.util.Scanner; public class MuestraNaturales { public static void main(String[] args) Scanner scanner = new Scanner(System.in); int n = scanner.nextInt(); muestraNaturales(n); } private static void muestraNaturales(int n) if( n>0 ) // invocación recursiva muestraNaturales(n-1); System.out.println(n); Análisis El método muestraNaturales recibe el valor del parámetro n. Si este valor es mayor que cero entonces se invoca a sí mismo con el argumento n-1. En algún momento n-1 será 0, por lo que el método no ingresará al if e irá directamente al System.out.println, donde mostrará el valor de n (que es 0) y retornará. Al retornar, hará que la invocación anterior salga del if y llegue al System.out.println para mostrar el valor de n que, en este caso, es 1. Así sucesivamente hasta que finalicen todas las invocaciones recursivas del método mostrarNaturales. Si el usuario ingresa el valor 4 entonces el programa arrojará la siguiente salida: 1 2 3 4

10 6.4 Permutar los caracteres de una cadena
Ahora se buscará desarrollar un programa que muestre por consola todas las permutaciones que se pueden realizar con los caracteres de una cadena de cualquier longitud. La cadena la ingresará el usuario por consola o por línea de comandos y no tendrá caracteres repetidos.  Por ejemplo: si la cadena ingresada fuese “ABC” entonces el programa debería arrojar las siguientes 6 líneas, en cualquier orden: ABC ACB BAC BCA CAB CBA Y si la cadena fuese: “ABCD” entonces la salida debería ser: ABCD ABDC ACBD ACDB ADBC ADCB BACD BADC : Evidentemente, la complejidad del problema crece conforme crece la longitud de la cadena que vamos a procesar. Por ejemplo, si el usuario ingresa una cadena s = "AB" entonces la solución sería trivial ya que alcanzaría con mostrar s.charAt(0) seguido de s.charAt(1) y luego mostrar s.charAt(1) seguido de s.charAt(0). Si la cadena ingresada tuviese tres caracteres la complejidad del problema aumentaría considerablemente y, al agregar un cuarto carácter, el problema se tornaría verdaderamente difícil de resolver.   De ese modo, el análisis de este ejercicio nos permitirá apreciar cómo a través de un proceso recursivo podemos reducir la complejidad de un problema hasta trivializarlo.

11 6.5 Búsqueda binaria El algoritmo de la búsqueda binaria puede plantearse como una función recursiva. Esta función recibirá el array arr, el valor v que se quiere buscar y dos índices, i y j, que delimitarán las posiciones de inicio y fin. Independientemente, de qué valores contengan los índices i y j, siempre vamos a comparar a v con el valor que se encuentre en la posición promedio del array (k=(i+j)/2). Luego, descartaremos la mitad del array anterior a k o la mitad posterior a esta posición dependiendo de qué v sea mayor o menor que arr[k].  Imaginemos el siguiente array arr, ordenado ascendentemente. Las posiciones de inicio y fin son i=0 y j=15, esta última coincide con arr.length-1. Para determinar si arr contiene el valor v=14 obtenemos la posición promedio calculándola como k=(i+j)/2. En este caso k será (0+15)/2 = 7. En la posición 7 encontraremos el valor 18 que, al ser mayor que v, nos permite descartar esta posición y todas las posiciones posteriores porque, al tratarse de un array ordenado, de estar el valor 14 estará en alguna posición anterior. 3 4 6 8 9 12 14 18 21 25 29 30 36 38 41 47 1 2 5 7 10 11 13 15

12 6.5 Búsqueda binaria Ahora invocamos a la función recursiva pasándole el array arr y el valor que buscamos, v. Para descartar la mitad posterior a k pasaremos los índices i=0, j=k-1. Si ahora repetimos la operación, la posición promedio será k=3. Como arr[k] es menor que v estamos en condiciones de descartar todas las posiciones anteriores. Para esto invocamos a la función recursiva pasándole arr, v y los valores i=k+1 y j. La posición promedio ahora será k=5. Como arr[k] es menor que v, de estar el 14, debería ser en alguna posición posterior. Luego, en la posición promedio k=6, arr[k] contiene al valor que buscábamos. 3 4 6 8 9 12 14 18 21 25 29 30 36 38 41 47 1 2 5 3 4 6 8 9 12 14 18 21 25 29 30 36 38 41 47 5 3 4 6 8 9 12 14 18 21 25 29 30 36 38 41 47

13 6.6 Ordenamiento por selección
Una técnica para ordenar arrays consiste en seleccionar el elemento más pequeño y permutarlo por el que se encuentra en la primera posición, luego repetir la operación descartando la posición inicial del array porque ésta, ahora, ya contiene su elemento definitivo. Este algoritmo puede implementarse como una función recursiva que reciba como parámetros el array y una posición dd desde la cual debemos procesarlo. Los elementos comprendidos entre las posiciones 0 y dd no deben ser considerados porque estarán ordenados.

14 6.7 La función de Fibonacci
La función de Fibonacci es una función recursiva que se define de la siguiente manera: Sea x perteneciente al conjunto de los números naturales, entonces: fibonacci(x) = fibonacci(x-1) + fibonacci(x-2), para todo x>=3 fibonacci(x) = 1, si x=1 o x=2 Si tomamos valores de x consecutivos comenzando desde 1 entonces, al invocar a fibonacci(x) obtendremos la sucesión de Fibonacci cuyos primeros 10 términos son los siguientes: Esta serie numérica tiene la siguiente característica: a partir de la posición 3, cada término coincide con la suma de los dos términos anteriores. Esta observación nos permite continuar la serie agregando tantos términos como queramos. Por ejemplo, el término 11 se calcula como = 89. El término 12 será: = 144, y así sucesivamente.

15 6.7 La función de Fibonacci
La lógica del algoritmo recursivo es trivial y no requiere explicación. Respecto del algoritmo iterativo, consiste en mantener dos variables, r1 y r2, con los primeros valores de la serie que, por definición, son: fibonacci(2) = r2 = 1 y fibonacci(1) = r1 = 1. Luego, dentro del for calculamos f como la suma de r2+r1. Para obtener el siguiente término descartamos r1 y consideramos que r2 será el nuevo r1 y que f será el nuevo r2. Así, f siempre tendrá la suma de los dos últimos números de la sucesión de Fibonacci.

16 6.7.1 Optimización del algoritmo recursivo de Fibonacci
Como vimos, la función recursiva de Fibonacci resulta ineficiente en una computadora porque debe resolver varias veces lo mismo.  Recordemos: para calcular f(50) primero debe resolver f(49) y f(48), pero para calcular f(49) primero debe resolver f(48) y f(47), y para calcular f(48) primero debe resolver f(47) y f(46).  Sin embargo, podemos optimizar la función “ayudándole” a recordar los términos que ya calculó y de esta manera evitar que los tenga que volver a calcular. Para esto utilizaremos una hashtable en la que almacenaremos los términos de la sucesión a medida que la función los vaya calculando. private static double fibonacci(int x,Hashtable<Integer,Double> t) { // primero verificamos si el resultado esta en la tabla Double d = t.get(x); // si no estaba entonces lo calculamos y lo ingresamos en la tabla if( d==null ) d = fibonacci(x-1, t)+fibonacci(x-2, t); t.put(x, d); } // retornamos el resultado return d; Notemos que la tabla debe inicializarse con las filas correspondientes a fibonacci(1) = fibonacci(2) = 1. Con esta mejora, el rendimiento de la función recursiva es comparable al rendimiento de la implementación iterativa.

17 6.7.1 Optimización del algoritmo recursivo de Fibonacci
Veamos entonces un programa que muestra los primeros n términos de la serie de Fibonacci siendo n un valor que ingresa el usuario. package mod4.cap01; import java.util.Hashtable; import java.util.Scanner; public class FibRecursivoOptimizado { public static void main(String[] args) // inicializamos la tabla Hashtable<Integer, Double> t = new Hashtable<Integer, Double>(); t.put(1, 1d); // 1d => 1 convertido a double t.put(2, 1d); // 1d => 1 convertido a double // el usuario ingresa el valor de n Scanner scanner = new Scanner(System.in); System.out.print("Cuantos terminos quiere ver: "); int n = scanner.nextInt(); for( int i=1; i<=n; i++) double f = fibonacci(i,t); System.out.println("fib(" + i + ") = " + f); } // : // aqui va la funcion...


Descargar ppt "Capítulo 17: Recursividad."

Presentaciones similares


Anuncios Google