recursion - UTN

Anuncio
Apuntes teóricos – Sintaxis y Semántica del Lenguaje
Año 2013
Prof. Lic. María Gabriela Cerra
RECURSION
Veremos un nuevo mecanismo, una nueva técnica de diseño, para resolver
problemas: LA RECURSIÓN.
La recursión es una alternativa a la iteración o repetición, y aunque en
tiempo de ejecución y espacio de memoria ocupada la solución recursiva es menos
eficiente que la iterativa, existen numerosas situaciones en las que la recursividad
provee una solución más simple y natural a un problema.
La recursividad es una herramienta potente y útil que la aplicaremos:
- en la resolución de problemas que tengan naturaleza recursiva
- en reemplazo de la iteración cuando el lenguaje de programación elegido
NO posea ninguna estructura de control repetitiva
- cuando la solución iterativa sea de gran complejidad respecto de la
solución recursiva
¿Qué es recursión? Es una técnica que realiza una tarea T, haciendo otra
tarea T ‘ de la misma naturaleza que T, pero en algún sentido más pequeña que la
original .
De esta forma, un algoritmo recursivo expresa la solución de un problema
de tamaño N, en términos de una llamada o invocación a sí mismo. Cada invocación
se plantea sobre problemas de igual naturaleza que el original, pero de un tamaño
menor que N. Al ir reduciendo progresivamente la complejidad del problema a
resolver, llegará un momento en que la resolución será trivial y directa. Esta última
situación se denomina caso base. La forma en que se va reduciendo el tamaño del
problema original asegura que el caso base se alcance y por consiguiente, se llegue
a la solución esperada.
Un algoritmo recursivo (procedimiento o función) presenta las siguientes 3
características:
- se autoinvoca dentro de su propia definición, es decir se llama a sí
mismo dentro de su cuerpo (al menos una vez)
- presenta al menos un caso base o especial, donde se llevan a cabo
acciones distintas que aseguran la finalización del proceso y la obtención
de la solución
- en cada autoinvocación se resuelve un problema de igual naturaleza que
el original pero de menor tamaño. La reducción del tamaño del problema
asegura que se alcance el caso base.
Se deben hacer cuatro preguntas para construir una solución recursiva:
1.- Cómo representar el problema T en términos de un problema T ’ del
mismo tipo, pero más pequeño.
1
Apuntes teóricos – Sintaxis y Semántica del Lenguaje
Año 2013
Prof. Lic. María Gabriela Cerra
2.- Cómo reducir, en cada llamada recursiva, el tamaño 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.
Para analizar esta técnica de diseño desde el punto de vista del uso de
memoria veamos el siguiente ejemplo.
Cálculo del Factorial, se elige porque es fácil de entender y se ajusta
perfectamente al modelo dado.
Definición iterativa del factorial (con n entero positivo):
FACT (n) = n * (n-1) * (n-2) * ....* 1
FACT (0) = 1
y el factorial de un número negativo es indefinido.
Todos sabemos construir una solución iterativa para este problema
basándonos en esta definición. También podemos construir un solución recursiva
del factorial:
FACT (n) = n * FACT (n - 1)
Esta definición carece de un elemento importante, el caso base.
Como en el diccionario, un caso debe definirse diferente de todos los
demás, de lo contrario la recursión nunca se detiene.
El caso base en la recursión 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.
Definición recursiva del factorial:
1
Factorial (n)
si n = 0
n * Factorial (n -1) si n > 0
Estudiaremos los mecanismos de ejecución de esta función recursiva.
Si calculamos el Factorial (3), usando esta definición:
Factorial (3) = 3 * Factorial (2)
2
Apuntes teóricos – Sintaxis y Semántica del Lenguaje
Año 2013
Prof. Lic. María Gabriela Cerra
Factorial (2) = 2 * Factorial (1)
Factorial (1) = 1 * Factorial (0)
Factorial (0) = 1
Se alcanzó el caso base. La aplicación de la definición recursiva se detiene y
la información obtenida se puede usar para responder la pregunta original
¿factorial (3)?
Dado que:
Factorial (0) = 1 entonces, reemplazando en cada llamada
Factorial (1) = 1 * 1 = 1, entonces
Factorial (2) = 2 * 1 = 2, entonces
Factorial (3) = 3 * 2 = 6
Es fácil construir una función a partir de la definición recursiva:
Function Factorial (n: entero) : entero
hacer
si (n=0) entonces Factorial :=1
sino Factorial := n * Factorial (n-1)
finsi
finhacer
finfunción
Esta función responde al modelo de solución recursiva.
1) La función Factorial se llama a sí misma
2) En cada llamada recursiva el número 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.
Tipos de recursión.
La recursión puede ser:
 Directa: si el algoritmo recursivo presenta una o más llamadas
recursivas en su propio cuerpo.
o Simple: si presenta una sola llamada recursiva
El factorial de un nº entero positivo:
Fact(0):=1
Fact(N):= N* fact(N-1), si N>0
o Múltiple: si presenta dos o más llamados recursivos
La serie de Fibonacci:
F(0):=0
F(1):=1
F(N):= F(N-1) + F(N-2), si N>1
3
Apuntes teóricos – Sintaxis y Semántica del Lenguaje
Año 2013
Prof. Lic. María Gabriela Cerra
o Anidada: si presenta un llamado recursivo como argumento de
una llamada recursiva
La función de Ackerman:
A(m,n):= n +1, si m=0
A(m,n):=A(m-1,1), si n=0
A(m,n):=A(m-1, A(m,n-1)), si m>0 y n>0

Indirecta o cruzada: si el llamado recursivo no aparece en su cuerpo
sino que se da a través de la invocación de un algoritmo auxiliar. Es
decir, si un algoritmo A invoca a otro algoritmo B y recíprocamente,
el algoritmo B invoca al algoritmo A.
Procedimiento A
Hacer
….
B
….
finhacer
finProcedimiento
Procedimiento B
Hacer
…
A
….
…
finhacer
finProcedimiento
¿Cómo funciona la recursión en memoria?. Traza de ejecución: método de
la caja
En principio la evaluación de un algoritmo recursivo no es más difícil que la
evaluación de cualquier otro algoritmo. En la práctica, sin embargo, el seguimiento
puede irse de las manos, para ello introducimos un método sistemático, llamado
método de la caja, para seguir la ejecución de funciones o procedimientos
recursivos.
Cada caja muestra la representación en memoria de la activación de una
unidad de programa y se denomina Registro de Activación de la unidad. Este
contiene toda la información necesaria para que dicha unidad pueda llevar a cabo su
ejecución: datos + información de control
* Cada llamada recursiva hecha al subprograma en el transcurso de la
ejecución va a generar una caja o registro de activación, que contendrá el
4
Apuntes teóricos – Sintaxis y Semántica del Lenguaje
Año 2013
Prof. Lic. María Gabriela Cerra
ambiente local del subprograma. Esto es, las variables y parámetros que se crean
en el llamado y se destruyen cuando se termina la ejecución.
Cada caja contendrá entonces:
-El valor de los parámetros formales.
-Las variables declaradas localmente (no existen en este ejemplo).
-Un lugar para el valor a ser retornado por cada llamada recursiva generada
a partir de la caja corriente (marcada con el rótulo).
-El valor de la función misma.
* 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
(rótulo) para indicar a donde se debe retornar.
* Comenzar la ejecución del cuerpo del subprograma con los valores
correspondientes al ámbito local de la caja corriente. Cuando termina la ejecución
de la caja corriente y se vuelve hacia atrás 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 ejecución del subprograma. El valor calculado se coloca en el ítem
apropiado en la caja corriente.
Trabajando sobre este concepto vamos a seguir la Traza de Ejecución que resulta
de calcular el Factorial (3):
Llamada original: Factorial (3) comienza la ejecución
n
=3
A: Factorial (n-1) = ?
Factorial
=?
rótulo
En el punto A, se hace una llamada recursiva, y la nueva invocación de la función
Factorial comienza su ejecución.
n
=3
A: Factorial (n-1) = ?
Factorial
=?
n
=3
A: Factorial (n-1) = ?
Factorial (n-1)
=?
A
A
n
=2
A: Factorial (n-1) = ?
Factorial
=?
n
=2
A: Factorial (n-1) = ?
Factorial
=?
En el punto A, nuevamente se
hace una llamada recursiva, y la
nueva invocación de la función
Factorial comienza su ejecución.
A
n
= 13
A: Factorial (n-1) = ?
Factorial (n-1)
=?
Nuevamente, en el punto A, se hace una llamada recursiva, y la nueva invocación de
la función Factorial comienza su ejecución.
5
Apuntes teóricos – Sintaxis y Semántica del Lenguaje
Año 2013
n
=3
A: Factorial (n-1) = ?
Factorial (n-1)
=?
A
n
=2
A: Factorial (n-1) = ?
Factorial
=?
Prof. Lic. María Gabriela Cerra
A
n
= 13
A: Factorial (n-1) = ?
Factorial (n-1)
=?
A
n
= 03
A: Factorial (n-1) = ?1
Factorial
Factorial (n-1)
=?
Se alcanza el caso base, por lo tanto la invocación 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 rótulo A)
n
=3
A: Factorial (n-1) = ?
Factorial (n-1)
=?
A
n
=2
A: Factorial (n-1) = ?
Factorial
=?
n
=3
A: Factorial (n-1) = ?
Factorial (n-1)
=?
A
n
=2
A: Factorial (n-1) = 1
Factorial
=2
n
=3
A: Factorial (n-1) = 2?
Factorial (n-1)
= 6?
A
n
=2
A: Factorial (n-1) = 1
Factorial
=2
A
A
A
n
= 13
A: Factorial (n-1) = 1?
Factorial (n-1)
= 1?
n
= 13
A: Factorial (n-1) = 1?
Factorial (n-1)
= ?1
n
= 13
A: Factorial (n-1) = 1?
Factorial (n-1)
= 1?
A
A
A
n
= 03
A: Factorial (n-1) = ?1
Factorial
Factorial (n-1)
=?
n
= 03
A: Factorial (n-1) = ?1
Factorial
Factorial (n-1)
=?
n
= 03
A: Factorial (n-1) = ?1
Factorial
Factorial (n-1)
=?
Valor final retornado al programa principal: 6
Otra forma de representación gráfica del método de las caja (pila de ejecución):
Conclusión: Como se puede apreciar en los gráficos del ejemplo anterior, el espacio
de memoria necesario para ejecutar un algoritmo recursivo es mucho mayor que si
fuese un algoritmo iterativo. En un algoritmo recursivo se genera una caja o
6
Apuntes teóricos – Sintaxis y Semántica del Lenguaje
Año 2013
Prof. Lic. María Gabriela Cerra
registro de activación por cada llamada al mismo subprograma, representando en
memoria los datos necesarios para su ejecución tantas veces como sea invocado. En
un algoritmo iterativo solo existe una caja o registro de activación
correspondiente al algoritmo que contiene dicha iteración.
Ejemplos de aplicación:
1)
Vamos a resolver de manera recursiva el problema de imprimir una lista de
caracteres hacia atrás.
Para ello debemos responder las tres preguntas, es decir vamos a construir
la solución al problema de imprimir una lista de longitud N hacia atrás en términos
de una cadena de longitud N -1. Esto indicaría que en cada paso recursivo la
longitud de la lista se hace más chica. Entonces el problema de imprimir una “lista
muy pequeña” hacia atrás puede servir como caso degenerado. Una lista muy
pequeña es la lista vacía, es decir de longitud cero. Nuestro caso base es entonces
imprimir una lista nula y la solución a este problema es no hacer nada (no hay nada
que imprimir).
Ahora hay que determinar de qué manera se puede usar la solución de
imprimir hacia atrás un lista de longitud N–1 para resolver el problema de imprimir
hacia atrás una lista de longitud N. La lista de longitud N –1 resulta de quitar un
nodo de la lista original avanzando en cada llamada, al nodo siguiente.
Veamos la siguiente solución aproximada:
Procedimiento Imprimir_ hacia_ atrás (PC)
Si (PC No es nulo) entonces
Imprimir_hacia_atrás ( nodo siguiente de PC)
Imprimir el carácter del nodo corriente apuntado por PC
finsi
Las llamadas recursivas a Imprimir _hacia _atrás pasan sucesivamente
listas de longitud más chicas sacando siempre un nodo al pasar como argumento la
dirección del nodo siguiente al corriente, entonces seguro se alcanzará el caso
base (es decir la lista vacía).
Procedure Imprimir_ hacia_ atrás (PC: lista)
Begin
If (PC <> nil) then
begin
Imprimir_hacia_atrás ( PC^.psig)
Writeln(PC^.let)
End
End;
Invocación desde el principal: Imprimir_hacia_atrás(Pin)
7
Apuntes teóricos – Sintaxis y Semántica del Lenguaje
Año 2013
Prof. Lic. María Gabriela Cerra
Traza de la ejecución usando el método de la caja:
Supongamos sea esta la lista L:
‘C’
‘A’
‘S’
‘A’
pc
pc
pc
pc
Write
‘C’
Write
‘A’
Write
‘S’
Write
‘A’
NIL
L
Imprime ‘a’
imprime ‘s’
Resultado  ‘ASAC’
imprime ‘a’
imprime ‘c’
pc
no hace nada
La construcción de la solución en este ejemplo y en el del factorial se logra
por backtracking, es decir, en la vuelta atrás del proceso recursivo. La
retroalimentación es una de las técnicas esenciales de resolución de la recursión,
eficaz a la hora de obtener todos los posibles resultados o combinaciones posibles
a un problema dado.
Por ejemplo para encontrar todas las combinaciones de números que
cumplen una determinada condición, o todas las ubicaciones posibles de fichas en
un tablero o la mejor solución a un problema, podemos “sacar” después de la
llamada recursiva el último elemento colocado en la combinación y al volver hacia
atrás el mecanismo probará con otro elemento aún no colocado. Este proceso se
repetirá recursivamente y se lograrán combinaciones automáticas.
2)
Diseñe una solución recursiva que encuentre todas las combinaciones
posibles de 3 letras mayúsculas de la A a la Z que admita letras repetidas.
Procedure Comb_Letras (cad:string[3]; N:integer);
{recibe una cadena nula en la que irá armando las posibles combinaciones y N=3, la
longitud de la cadena a armar}
Var
I: char;
Begin
If (N=0) then
Writeln(cad)
Else
For I:=’A’ to ‘Z’ do
Comb_Letras (cad + I, N-1) {concatena una letra a la cadena
y decrementa su longitud)
End;
Invocación desde el principal: Comb_Letras ( ‘ ‘, 3)
8
Apuntes teóricos – Sintaxis y Semántica del Lenguaje
Año 2013
Prof. Lic. María Gabriela Cerra
Este algoritmo usa la iteración para lograr generar todas las letras que se colocan
en una posición dada de la cadena que se está armando. Observar que la lógica de
resolución propone ubicar una letra inicial en la cadena (concatenando) comenzando
con la A y llamar recursivamente para ubicar una letra en la 2º posición y lo mismo
con la siguiente. Cuando se hayan puesto las primeras 3 letras, llega al caso base e
imprime (AAA). Por backtracking deshace una caja al volver atrás y ahora va
reemplazando la última letra por B e imprime, luego por C e imprime, y así siguiendo
hasta llegar a la Z.
AAA AAB
AAC ……AAZ
Al finalizar la ejecución del For para la 3º posición, retorna una paso más atrás y
vuelve a la 2º posición y cambia la letra por B y vuelve a completar la 3º posición……
ABA ABB
ABC …..ABZ
ACA ACB
ACC ….ACZ
………
AZA
AZB
……….
AZZ
BAA BAB
BAC
BAZ
BBA ………………………………………………...
…..
ZAA ZAB
…..ZAZ
…
ZZA ZZB …………………………… ZZZ
Es decir, por cada letra que ubica en una posición dada, la combina con todas las 28
posibles del alfabeto en la posición siguiente. Esto se logra combinando
backtracking y el uso del For.
3)
Diseñe una solución recursiva que permita eliminar un elemento de una lista.
Procedure Elim( var L:lista; pa:lista; pc:lista; el:elem);
Begin
If (pc <> nil) then
If (pc^.dato < > el) then
Elim (L, pa^.ps, pc^.ps, el)
Else
Begin
If (pa=nil) then
L:=L^.ps;
Else
Pa^.ps:=pc^.ps;
Dispose(pc)
End
End;
Invocación desde el principal: Elim(Pin, nil, Pin, eldato)
9
{lista simple}
Apuntes teóricos – Sintaxis y Semántica del Lenguaje
Año 2013
Prof. Lic. María Gabriela Cerra
4)
Diseñe una solución recursiva que permita insertar al final un elemento en
una lista circular.
Procedure InsCir( var L:listaC; pc:listaC; el:elem);
Var
Nuevo:listaC;
Begin
If (L=nil) then {lista vacia}
Begin
New(nuevo);
nuevo^.dato:=el;
L:= nuevo;
nuevo^.ps:=L;
end
else
If (pc^.ps <> L) then
InsCirc(L, pc^.ps, el)
Else
Begin
New(nuevo);
Nuevo^.dato:=el;
Pc^.ps:=nuevo;
Nuevo^.ps:=L
End
{lista circular}
end
Invocación desde el principal: InsCir( Pin, Pin, eldato)
Ventajas y desventajas del uso de recursión.
Las soluciones recursivas son elegantes y simples para problemas de
complejidad grande, presentan un diseño muy bien estructurado y modular.
Comparadas con la solución iterativa contienen menos líneas de código y son más
fáciles de analizar y leer.
Desde el punto de vista de la eficiencia, demandan más tiempo de ejecución
que las soluciones iterativas y mayor espacio de memoria debido a que cada llamado
recursivo genera una copia independiente de las variables declaradas en dicho
algoritmo, almacenadas en una zona de la memoria Ram denominadas Registro de
Activación.
Aplicación y uso.
Esta técnica de diseño se aplica fundamentalmente en lenguajes de
programación que carecen de estructuras de control repetitivas como ocurre en
aquellos pertenecientes a la programación Funcional (LISP, HASKELL) y Lógica
10
Apuntes teóricos – Sintaxis y Semántica del Lenguaje
Año 2013
Prof. Lic. María Gabriela Cerra
(APL, PROLOG). Es de amplio uso en Inteligencia Artificial como demostrador de
teoremas, y en el área matemática para los cálculos combinatorios.
Bibliografía de referencia:
- Luis Joyanes Aguilar, 2003. Fundamentos de Programación. Algoritmos y
Estructuras de Datos. Ed. Mc Graw Hill
- Luis Joyanes Aguilar, 2008. Fundamentos de Programación. Algoritmos, Estructuras
de Datos y Objetos. Ed. Mc Graw Hill
- Luis Joyanes Aguilar, 2006. Programación en Turbo/Borland Pascal 7.0. Ed. Mc
Graw Hill
- De Giusti, Madoz y otros,1998. Algoritmos, datos y programas. Conceptos básicos.
Ed. Exacta.
11
Descargar