Guía Corta: Recursión de Cola 1. Preliminares: Repetición vs

Anuncio
Universidad Simón Bolívar
Departamento de Computación y Tecnología de la Información
CI3641 Lenguajes de Programación I
Guía Corta: Recursión de Cola
Esta guía presenta algunos conceptos básicos y ejemplos de Recursión de Cola, incluyendo
como reconocerla y transformaciones de código asociadas a la misma.
Nota: Es importante recordar que esta guía es un complemento, cuyo objetivo es ayudar al
entendimiento de estos conceptos y su aplicación en la práctica. No debe considerarse un reemplazo
para la bibliografía ocial del curso.
1.
Preliminares: Repetición vs. Recursión
La repetición y la recursión son técnicas ofrecidas por algunos lenguajes de programación que
permiten ejecutar una cantidad determinada (o posiblemente indeterminada) de veces un conjunto
de instrucciones. La recursión es natural de los lenguajes de programación declarativos, mientras
que la repetición es natural en lenguajes de programación imperativos. A pesar de esto, muchos
lenguajes imperativos ofrecen facilidades para escribir funciones y procedimientos recursivos, ya
que algunos problemas son más fáciles de expresar recursivamente. Los lenguajes declarativos
puros (en donde existe transparencia referencial) no necesitan suministrar repeticiones, pues los
mismos no serían útiles sin efectos de borde.
Factorial Recursivo
Factorial Iterativo
int fact (int n) {
int ret = 1;
while (n-- > 0)
ret *= n
return ret;
}
int fact (int n) {
if (n == 0)
return 1;
return n * fact(n - 1);
}
Nota: Un programa con ciclos es iterativo. Cada uno de estos ciclos se llaman repeticiones
y cada una de las vueltas que realizan dichas repeticiones es una iteración.
2.
Recursión de Cola
Los defensores de los lenguajes de programación imperativos, tras no poder convencer vendiendo la facilidad de implementación, atacan a los defensores de lenguajes funcionales con que
la repetición es naturalmente más eciente que la recursión. Sin embargo, tal armación no está
completa:
Una repetición es más eciente que una recursión ingenua.
1
¾Qué podría hacer más lenta la recursión que la repetición? Una respuesta razonable es que
la invocación de funciones supone empilar y desempilar parámetros, variables locales, valores
de retorno, etc. Esto es cierto y sin duda podría presentar un overhead importante, si no fuera
porque dicho problema se puede evitar con un poco de astucia de parte de los compiladores e
intérpretes.
Si una llamada recursiva en particular es la última acción que debe hacer una procedimiento
(o el último valor que debe evaluar una función), entonces el marco de pila del invocador puede ser
descartado y reutilizado por el invocado. El marco de pila antiguo no tiene por que conservarse,
ya que el estado que guarda el mismo no es utilizado al regresar de la invocación recursiva.
2.1.
La Receta: Recursión de Cola
→
Repetición
Si se tiene una subrutina recursiva de cola, la misma puede ser traducida directamente en
una versión iterativa de la misma. Para esto, se debe analizar el programa en cuestión.
1. El cuerpo de la función es reemplazado por una repetición indeterminada, en donde el
guardia es la disyunción de todas las condiciones que puedan llevar a una llamada recursiva
de cola.
2. Las llamadas recursivas de cola serán reemplazadas por asignaciones, en donde al iésimo
parámetro formal de la subrutina se le asigna el iésimo argumento de la llamada en cuestión.
3. Las demás vías de cómputo de la subrutina (los casos bases) se moverán al nal de la
repetición, justamente saliendo de la misma.
Como ejemplo, consideremos un programa que calcula el máximo común divisor de dos números usando el Algoritmo de Euclides.
MCD Recursivo de Cola
MCD Iterativo
int mcd (int a, int b) {
while ( b != 0 ) {
int t = a;
a = b;
b = t % b;
}
return a;
}
int mcd (int a, int b) {
if ( b == 0 )
return a;
return mcd(b, a % b);
}
Nótese que en la transformación propuesta, se creó una variable temporal t para conservar
el valor anterior de a en la asignación. En un lenguaje que permita asignación múltiple, se
podría reescribir simplemente como: (a, b = b, a % b). Además, el guardia de la repetición es
(b != 0), ya que es la condición implícita necesaria para que se ejecute la llamada recursiva de
cola.
2
2.2.
La Receta: Recursión
→
Recursión de Cola
Convertir un programa recursivo en uno que tenga recursión de cola es altamente no trivial
y cada caso se resuelve de una manera distinta. Sin embargo, para funciones que reciben un solo
argumento y donde la operación g que evita la recursión de cola es la misma en cada caso, la
transformación necesaria para tener recursión de cola es similar.
1. Se dene una subrutina auxiliar con el mismo parámetro que la original, mas un acumulador
(del tipo de retorno en el caso de funciones), donde el cuerpo de la función es el mismo de
la función original.
2. Las llamadas recursivas serán reemplazadas por llamadas a la subrutina auxiliar, donde la
acción g que evitaba la recursión de cola es pasada al acumulador. Esto es:
return g(f(args), x)
return f_aux(args, g(acum, x))
→
3. Cada caso no recursivo es reemplazado por la operación g aplicado al mismo y el acumulador. Esto es:
return x
return g(acum, x)
→
4. El cuerpo de la subrutina original es substituido por una invocación a la subrutina auxiliar,
con el mismo argumento y con el valor neutro de la operación g como acumulador.
En el caso de la segunda y tercera transformación debe tenerse cuidado con el orden de los
argumentos para g . En el caso de operaciones no conmutativas (como la concatenación de listas)
puede ser necesario invertir el orden de los argumentos. Como ejemplo, consideremos un programa
que calcula el factorial de un número.
Factorial Recursivo
Factorial Recursivo de Cola
int fact (int n) {
return fact(n, 1 );
}
int fact (int n) {
if (n == 0)
return 1 ;
return n * fact( n - 1 );
}
int fact_aux(int n, int acum) {
if (n == 0)
return acum ;
return fact_aux( n - 1 , n * acum);
}
Nótese que el caso no recursivo, las reglas planteadas sugerirían tener 1 * acum. Pero siempre
se pueden simplicar las expresiones que son triviales (las cuales aparecerán usualmente).
3
Como un segundo ejemplo, consideremos un programa que hace el reverso de una lista.
Revertir Recursivo
[T] revertir ([T] lista) {
if (lista == [])
return [] ;
return revertir( lista.cola ) ++ [lista.cabeza] ;
}
Revertir Recursivo de Cola
[T] revertir ([T] lista) {
return revertir(lista, [] );
}
[T] revertir_aux([T] lista, [T] acum) {
if (lista == [])
return acum ;
return revertir_aux( lista.cola , [lista.cabeza] ++ acum);
}
En este caso, la concatenación debió cambiar el orden de los argumentos al pasar a recursión
de cola. ¾Qué pasaría si no se hubiera invertido el orden de la concatenación?
3.
Ejericios sugeridos
Casi todos los segundos exámenes de la materia, como se ha dado durante los últimos años por
el prof. Ernesto HernandezNovich y mi persona, tienen una o más preguntas sobre transformación
de programas concernientes a la recursión de cola. Les recomiendo busquen esos parciales e
intenten resolverlos usando la receta correspondiente en cada caso. Pueden escribirme si tienen
cualquier duda al respecto.
Ricardo Monascal / Mayo 2014
4
Descargar