Algoritmos y Estructuras de Datos Apuntes tericos Prof. Lic.
Mara Gabriela Cerra
Ao 2009
1 RECURSION
Veremos un nuevo mecanismo para resolver problemas: RECURSIN. La recursin es una alternativa a la iteracin o repeticin, y aunque en tiempo de ejecucin y espacio de memoria ocupada la solucin recursiva es menos eficiente que la iterativa, existen numerosas situaciones en las que la recursividad provee una solucin ms simple y natural a un problema. La recursividad es una herramienta potente y til en la resolucin de problemas que tengan naturaleza recursiva. Estudiaremos recursin desde un punto de vista conceptual, y nos va a interesar discutir el mtodo para llegar a comprender el mecanismo de la recursin. Lo vamos a hacer a travs de ejemplos.
SOLUCIONES RECURSIVAS: Soluciones elegantes y simples para problemas de complejidad grande, muy bien estructurados y modulares.
Qu es recursin? Es una tcnica que realiza una tarea T, haciendo otra tarea T. Esta definicin parece semejante al diseo top-down, donde un problema grande se descompone en pequeos problemas. La diferencia es que en recursin, la tarea T es de la misma naturaleza que la tarea T. La pregunta ahora es qu ganamos resolviendo tareas mediante otra semejante? Lo importante es que en algn sentido T es ms pequea que T.
Veremos estos conceptos a travs de un ejemplo. Consideremos el problema de buscar una palabra en un diccionario. Una bsqueda binaria se puede formular como:
/*Buscar una palabra en el diccionario*/
Si diccionario tiene una sola pgina entonces Ubicar la palabra en esa pgina sino Abrir diccionario en punto cercano a la mitad Determinar a que mitad pertenece la palabra Si la palabra pertenece a la primera mitad entonces Buscar la palabra en la primera mitad del diccionario sino Buscar la palabra en la segunda mitad del diccionario Finsi finsi
Algoritmos y Estructuras de Datos Apuntes tericos Prof. Lic. Mara Gabriela Cerra Ao 2009
2 La solucin que hemos planteado est a un alto nivel de desarrollo, no nos interesa ahora entrar en detalles de implementacin. Lo que nos interesa es examinar la estrategia de esta solucin. Hemos reducido el problema de buscar una palabra en el diccionario a buscarla en una mitad del mismo.
Dos caractersticas importantes:
- Una vez dividido el diccionario, est claro cual ser la mitad en la que debemos buscar y se buscar utilizando la misma estrategia. - Hay un caso especial, que se maneja distinto de los dems, es el caso en que el diccionario ha sido dividido tantas veces que tiene slo una pgina. En este punto el problema es suficientemente pequeo y se puede resolver directamente. Este caso especial se llama caso base.
Podemos ver esta forma de resolver problemas como dividir y conquistar (El problema se resolvi primero dividiendo el diccionario en dos mitades y luego conquistando la mitad apropiada). El problema ms pequeo se resuelve aplicando la misma estrategia.
Escribimos la solucin como un procedimiento para resaltar algunas observaciones importantes.
Procedimiento Buscar (dic, pal) Si diccionario tiene una sola pgina entonces Ubicar la palabra en esa pgina sino Abrir diccionario en punto cercano a la mitad Determinar a que mitad pertenece la palabra Si la palabra pertenece a la primera mitad entonces Buscar (primera mitad del dic, pal) sino Buscar (segunda mitad del dic, pal)
Finsi Finsi
Buscar en el diccionario Buscar en la primera mitad del diccionario Buscar en la segunda mitad del diccionario Algoritmos y Estructuras de Datos Apuntes tericos Prof. Lic. Mara Gabriela Cerra Ao 2009
3
Observaciones:
1.- Una de las acciones de este procedimiento es llamarse a s mismo. Es decir, el procedimiento BUSCAR es llamado desde adentro del procedimiento BUSCAR. Esto es lo que hace una solucin recursiva.
2.- Cada llamada BUSCAR (diccionario, pal) pasa un diccionario de la mitad de tamao que el anterior. Es decir en cada llamada recursiva el tamao del diccionario se reduce. El problema de la bsqueda est siendo resuelto, resolviendo otra de igual naturaleza pero ms pequeo en tamao.
3.- Hay un problema de bsqueda que resuelve en forma diferente. Cuando el diccionario tiene una sola pgina, se resuelve por otro mtodo (aqu se busca directamente), este es el caso base. Cuando se alcanza el caso base, las llamadas recursivas se detienen y el problema se resuelve directamente. Lo importante es que la manera en la cual el tamao del problema disminuye, asegura que el caso base ser alcanzado.
Se deben hacer cuatro preguntas para construir una solucin recursiva
1.- Cmo representar el problema en trminos de un problema del mismo tipo, pero ms pequeo.
2.- Cmo reducir, en cada llamada recursiva, el tamao del problema.
3.- Qu instancia del problema sirve como caso base.
4.- Qu manera de reducir el problema nos asegura que siempre ser alcanzado el caso base.
Ejemplos de aplicacin.
Ejemplo 1: Clculo del Factorial, se elige porque es fcil de entender y se ajusta perfectamente al modelo dado.
Definicin iterativa del factorial (con n entero positivo):
FACT (n) = n * (n-1) * (n-2) * ....* 1
Por definicin FACT (0) = 1 y el factorial de un nmero negativo es indefinido.
Algoritmos y Estructuras de Datos Apuntes tericos Prof. Lic. Mara Gabriela Cerra Ao 2009
4 Todos sabemos construir una solucin iterativa para este problema basndonos en esta definicin. Tambin podemos construir un solucin recursiva del factorial:
FACT (n) = n * FACT (n - 1)
Esta definicin carece de un elemento importante, el caso base. Como en el diccionario, un caso debe definirse diferente de todos los dems, de lo contrario la recursin nunca se detiene. El caso base en la recursin es el Factorial (0) el que se define simplemente como 1. Dado que n se asume positivo, decrementando en 1 cada vez que se llama al factorial se sabe que siempre ser alcanzado el caso base.
1 si n = 0 Factorial (n) n * Factorial (n -1) si n > 0
Estudiaremos los mecanismos de ejecucin de esta funcin recursiva. Antes analizamos el Factorial = n* Factorial (n-1), esta sentencia tiene el siguiente efecto: Si calculamos el Factorial (4), usando esta definicin Factorial (4) = 4 * Factorial (3) Factorial (3) = 3 * Factorial (2) Factorial (2) = 2 * Factorial (1) Factorial (1) = 1 * Factorial (0) Factorial (0) = 1 Se alcanz el caso base. La aplicacin de la definicin recursiva se detiene y la informacin obtenida se puede usar para responder la pregunta original factorial (4)? Dado que: Factorial (0) = 1 entonces Factorial (1) = 1 * 1 = 1, entonces Factorial (2) = 2 * 1 = 2, entonces Factorial (3) = 3 * 2 = 6, entonces Factorial (4) = 4 * 6 = 24
Esta definicin recursiva del factorial ilustra dos cosas:
- intuitivamente, Factorial (n) se puede definir en trminos del Factorial (n-1) - mecnicamente, la definicin se puede aplicar para determinar el valor de un factorial dado. Algoritmos y Estructuras de Datos Apuntes tericos Prof. Lic. Mara Gabriela Cerra Ao 2009
5 Es fcil construir una funcin a partir de la definicin recursiva:
Function Factorial (n: entero) : entero hacer si (n=0) entonces Factorial :=1 sino Factorial := n * Factorial (n-1) finsi finhacer finfuncin
Esta funcin responde al modelo de solucin recursiva. 1) La funcin Factorial se llama a s misma 2) En cada llamada recursiva el nmero cuyo factorial se calcula se disminuye en 1. 3) El Factorial (0) se maneja en forma distinta. Este caso base no produce una llamada recursiva.
Estudiaremos los mecanismos de ejecucin de esta funcin recursiva: - Se evala la expresin a la derecha del smbolo : = - Como es un producto se evala cada operando. - El segundo operando es una llamada a la funcin Factorial, a pesar de ser una llamada recursiva no hay nada especial en ella. Si fuera otra la funcin, el principio es el mismo, se evala la funcin.
En principio la evaluacin de una funcin recursiva no es ms difcil que la evaluacin de otra funcin. En la prctica, sin embargo, el seguimiento puede irse de las manos, para ello introducimos un mtodo sistemtico, llamado mtodo de la caja, para seguir la ejecucin de funciones o procedimientos recursivos.
Mtodo de la Caja
* Rotular con una letra el llamado recursivo en el cuerpo del subprograma recursivo. En el ejemplo Factorial (n-1) lo llamamos A. Nos servir para saber exactamente a donde se debe retornar despus que la llamada a la funcin se complete. * Cada llamada recursiva hecha al subprograma en el transcurso de la ejecucin va a generar una nueva caja, que contendr el ambiente local del subprograma. Esto es, las variables y parmetros que se crean en el llamado y se destruyen cuando se termina la ejecucin.
Cada caja contendr entonces: -El valor de los parmetros formales. -Las variables declaradas localmente (no existen en el ejemplo). -Un lugar para el valor a ser retornado por cada llamada recursiva generada a partir de la caja corriente (marcada con el rtulo). -El valor de la funcin misma. Algoritmos y Estructuras de Datos Apuntes tericos Prof. Lic. Mara Gabriela Cerra Ao 2009
6 * Cuando se crea una nueva caja se dibuja una fecha desde la caja donde se hizo la llamada hacia la nueva. Sobre la flecha se pone el nombre de la llamada (rtulo) para indicar a donde se debe retornar. * Comenzar la ejecucin del cuerpo del subprograma con los valores correspondientes al mbito local de la caja corriente. Cuando termina la ejecucin de la caja corriente y se vuelve hacia atrs en las cajas, la anterior es ahora la corriente y el nombre en la flecha indica el lugar a donde se debe retornar y continuar la ejecucin del subprograma. El valor calculado se coloca en el tem apropiado en la caja corriente.
Ejemplo 1: vamos a calcular el Factorial (3)
Llamada original: Factorial (3) comienza la ejecucin
rtulo
En el punto A, se hace una llamada recursiva, y la nueva invocacin de la funcin Factorial comienza su ejecucin.
En el punto A, nuevamente se hace una llamada recursiva, y la nueva invocacin de la funcin Factorial comienza su ejecucin.
Nuevamente, en el punto A, se hace una llamada recursiva, y la nueva invocacin de la funcin Factorial comienza su ejecucin.
Se alcanza el caso base, por lo tanto la invocacin de Factorial se complet y pueden comenzar a resolverse las cajas. Se vuelve a la caja anterior y se retorna el valor pendiente al punto del llamado (marcado con el rtulo A)
n = 3 A: Factorial (n-1) = ? Factorial = ? n = 3 A: Factorial (n-1) = ? Factorial = ? n = 2 A: Factorial (n-1) = ? Factorial = ? A n = 3 A: Factorial (n-1) = ? Factorial (n-1) = ? n = 3 A: Factorial (n-1) = ? Factorial (n-1) = ? n = 2 A: Factorial (n-1) = ? Factorial = ? A n = 3 A: Factorial (n-1) = ? Factorial = ? n = 1 A: Factorial (n-1) = ? Factorial = ? A n = 3 A: Factorial (n-1) = ? Factorial (n-1) = ? n = 3 A: Factorial (n-1) = ? Factorial (n-1) = ? n = 3 A: Factorial (n-1) = ? Factorial (n-1) = ? n = 2 A: Factorial (n-1) = ? Factorial = ? A n = 3 A: Factorial (n-1) = ? Factorial = ? n = 1 A: Factorial (n-1) = ? Factorial = ? A n = 0 Factorial = 1 A Algoritmos y Estructuras de Datos Apuntes tericos Prof. Lic. Mara Gabriela Cerra Ao 2009
7
Valor final retornado al programa principal: 6
Ejemplo 2:
Vamos a resolver de manera recursiva el problema de imprimir una cadena de caracteres hacia atrs. Para ello debemos responder las tres preguntas, es decir vamos a construir la solucin al problema de imprimir una cadena de longitud n hacia atrs en trminos de una cadena de longitud n -1. Esto indicara que en cada paso recursivo la longitud de la cadena se hace ms chica. Entonces el problema de escribir una cadena muy pequea hacia atrs puede servir como caso degenerado. Una cadena muy pequea es la cadena vaca o nula, es decir la cadena de longitud cero. Nuestro caso base es entonces imprimir una cadena nula y la solucin a este problema es no hacer nada (no hay nada que imprimir). Ahora hay que determinar de qu manera se puede usar la solucin de imprimir hacia atrs un cadena de longitud n 1 para resolver el problema de imprimir hacia atrs una cadena de longitud n. La cadena de longitud n 1 resulta de quitar un carcter de la cadena original.
Veamos la siguiente solucin aproximada: Imprimir_ hacia_ atrs (S) Si (S es nula) entonces no hacer nada (es el caso base) sino Imprimir el ltimo carcter de S Imprimir_hacia_atrs (S menos el ltimo carcter) finsi
Las llamadas recursivas a Imprimir _hacia _atrs pasan sucesivamente cadenas de longitud ms chicas sacando siempre el ltimo carcter del anterior, entonces seguro se alcanzar el caso base (es decir la cadena nula). n = 3 A: Factorial (n-1) = ? Factorial (n-1) = ? n = 3 A: Factorial (n-1) = ? Factorial (n-1) = ? n = 3 A: Factorial (n-1) = ? Factorial (n-1) = ? n = 2 A: Factorial (n-1) = ? Factorial = ? A n = 3 A: Factorial (n-1) = ? Factorial = ? n = 1 A: Factorial (n-1) = 1 Factorial = 1 A n = 0 Factorial = 1 A n = 3 A: Factorial (n-1) = ? Factorial (n-1) = ? n = 2 A: Factorial (n-1) = 1 Factorial = 2 A n = 3 A: Factorial (n-1) = ? Factorial = ? n = 3 A: Factorial (n-1) = ? Factorial (n-1) = ? n = 1 A: Factorial (n-1) = 1 Factorial = 1 A n = 3 A: Factorial (n-1) = ? Factorial (n-1) = ? n = 0 Factorial = 1 A n = 3 A: Factorial (n-1) = ? Factorial (n-1) = ? n = 2 A: Factorial (n-1) = 1 Factorial = 2 A n = 3 A: Factorial (n-1) = 2 Factorial = 6 n = 3 A: Factorial (n-1) = ? Factorial (n-1) = ? n = 1 A: Factorial (n-1) = 1 Factorial = 1 A n = 3 A: Factorial (n-1) = ? Factorial (n-1) = ? n = 0 Factorial = 1 A Algoritmos y Estructuras de Datos Apuntes tericos Prof. Lic. Mara Gabriela Cerra Ao 2009
8 Traza de ejecucin usando el mtodo de la caja:
S = casa
Imprime a imprime s imprime a imprime c no hace nada
Resultado asac
Consideremos otra solucin al problema, quitar el primer carcter de la cadena (en lugar del ltimo).
Analicemos la siguiente solucin: Imprimir_ hacia_ atrs (S) Si (S es nula) entonces no hacer nada (es el caso base) sino Imprimir el primer carcter de S Imprimir_hacia_atrs (S menos el primer carcter) finsi
Hace lo que se espera? No, la recursin no es mgica, se debe formular correctamente la solucin. Veamos la solucin recursiva correcta: Imprimir_ hacia_ atrs (S) Si (S es nula) entonces no hacer nada (es el caso base) sino Imprimir_hacia_atrs (S menos el primer carcter) Imprimir el primer carcter de S finsi
Esto significa que escribimos el primer carcter recin cuando todo el resto ya ha sido escrito.
Traza de Escribir_ hacia_ atrs 2: en cada caja, luego del llamado recursivo imprime el 1 carcter
imprime c imprime a imprime s imprime a no hace nada S=casa S=cas S=ca S=c S= S=casa S=asa S=sa S=a S= Algoritmos y Estructuras de Datos Apuntes tericos Prof. Lic. Mara Gabriela Cerra Ao 2009
9 Resultado asac
El resultado es el mismo que con la otra solucin, imprime asac pero la secuencia de cadenas que se va generando es diferente. La diferencia en la secuencia de valores se compensa por el carcter que se imprime y en el momento en que se imprime. En la primera solucin se imprime antes de generar una nueva caja y en la segunda al salir de la caja antes de retornar de una llamada recursiva. El objetivo de este ejemplo es demostrar que podemos tener dos estrategias diferentes (o ms) para realizar las mismas tareas.
Consideremos un pequeo cambio al problema y supongamos que la cadena est almacenada en un arreglo. Si desarrollamos las dos soluciones vistas tendramos:
Tipos estructurados vec = arreglo [n]: carcter 1
Procedimiento Imprimir_HACIA_ATRS (s: vec; dim: entero 2); Hacer Si dim >0 entonces Imprimir: s[dim] Imprimir_HACIA_ATRS (s, dim-1) Finsi Finhacer Finprocedimiento /* el caso base, cadena nula no se escribe pues no se hace nada*/
La segunda solucin vista, llamar al procedimiento recursivo con la cadena sin su primer carcter y tambin se puede resolver sin inconvenientes si la cadena est almacenada en un arreglo de caracteres. Qu ocurre si la cadena est almacenada en una lista de caracteres en lugar de un arreglo?
Tipos estructurados lista = ^nodo Nodo = registro Letra: carcter 1 Prox: lista finregistro variables str: lista
La variable str de tipo lista apunta al primer nodo de la lista que contiene la cadena que se debe imprimir. Algoritmos y Estructuras de Datos Apuntes tericos Prof. Lic. Mara Gabriela Cerra Ao 2009
10 Si consideramos la primera solucin cuando llamamos recursivamente con la cadena sin su ltimo carcter, se hace difcil de implementar ya que lo que conocemos es la direccin del primer nodo. Para obtener la direccin del ltimo nodo y eliminarlo hay que recorrer toda la lista. Otra opcin sera mantener tambin un puntero al ltimo nodo, pero sera complicado direccionar al ante-ltimo nodo para eliminar el ltimo. Esto hace ms ineficiente la solucin. La segunda solucin vista requiere solo conocer la direccin de acceso al primer nodo de la lista:
Procedimiento imprimir_HACIA_ATRS2 (str: lista) hacer si str <> nil entonces imprimir_HACIA_ATRS2 (str^. prox) imprimir: str^. letra finsi finhacer finprocedimiento
Ejemplo 3: Resolver X elevado a la n
Ahora veremos como podemos encontrar una solucin recursiva, es decir cmo podemos definir X a la n-sima potencia en trminos de X a una potencia ms pequea. La respuesta est dada en las reglas de exponenciacin:
X N = X * X N-1 Esto es, podemos calcular X a la N, calculando X a la N-1 y multiplicando su resultado por X.
Nuestra solucin recursiva tiene un solo caso base: X 0 = 1
Entonces podemos formular nuestra solucin recursiva de la siguiente forma: X 0 = 1 X N = X * X N-1 , si N > 0 El caso base (N=0) siempre se alcanzar.
Funcin potencia (x : entero 2 ; N : entero 1); Hacer Si N=0 entonces potencia :=1 sino potencia := X * potencia (X, N-1) finsi finhacer finfuncin
Algoritmos y Estructuras de Datos Apuntes tericos Prof. Lic. Mara Gabriela Cerra Ao 2009
11 Bibliografa de referencia: - Fundamentos de Programacin. Algoritmos y estructuras de datos. Luis Joyanes Aguilar. - Programacin en Turbo/Borland Pascal 7.0, Luis Joyanes Aguilar - Algoritmos, datos y programas. Conceptos bsicos, De Giusti, Madoz y otros.
1.1.2 Sistemas de Referencia Inerciales y No Inerciales. Distancia y Desplazamiento. Rapidez y Velocidad. Rapidez Promedio y Rapidez Instantánea. Aceleración.