Sunteți pe pagina 1din 17

18.4.

La recursin y cmo
puede ser que funcione
Estamos acostumbrados a escribir funciones que llaman a otras
funciones. Pero lo cierto es que nada impide que en Python (y en
muchos otros lenguajes) una funcin se llame a s misma. Y lo ms
interesante es que esta propiedad, que se llama recursin, permite en
muchos casos encontrar soluciones muy elegantes para determinados
problemas.
En materias de matemtica se estudian los razonamientos por induccin
para probar propiedades de nmeros enteros, la recursin no es ms que
una generalizacin de la induccin a ms estructuras: las listas, las
cadenas de caracteres, las funciones, etc.
A continuacin estudiaremos diversas situaciones en las cuales aparece
la recursin, veremos cmo es que esto puede funcionar, algunas
situaciones en las que es conveniente utilizarla y otras situaciones

18.5. Una funcin


recursiva matemtica
Es muy comn tener definiciones inductivas de operaciones, como por
ejemplo:

x! = x (x 1)! si x > 0, 0! = 1

Este tipo de definicin se traduce naturalmente en una funcin en Python:


def factorial(n):
""" Precondicin: n entero >=0
Devuelve: n! """
if n == 0:
return 1

return n * factorial(n-1)

Esta es la ejecucin del factorial para n=0 y para n=3.


>>> factorial(0)
1
>>> factorial(3)
6

El sentido de la instruccin de la instruccin n * factorial (n-1) es


exactamente el mismo que el de la definicin inductiva: para calcular el
factorial dense debe multiplicar n por el factorial de n 1.
Dos piezas fundamentales para garantizar el funcionamiento de este
programa son:

Que se defina un caso base (en este caso la indicacin, no


recursiva, de cmo calcularfactorial(0)), que corta las llamadas
recursivas.

Que el argumento de la funcin respete la precondicin de


que n debe ser un entero mayor o igual que 0.

Dado que ya vimos la pila de evaluacin, y cmo funciona, no debera


llamarnos la atencin que esto pueda funcionar adecuadamente en un
lenguaje de programacin que utilice pila para evaluar.
Para poder analizar qu sucede a cada paso de la ejecucin de la
funcin, utilizaremos una versin ms detallada del mismo cdigo, en la
que cada paso se asigna a una variable.
def factorial(n):
""" Precondicin: n entero >=0
Devuelve: n! """
if n == 0:
r=1
return r

f = factorial(n-1)
r=n*f
return r

Esta porcin de cdigo funciona exactamente igual que la anterior, pero


nos permite ponerles nombres a los resultados intermedios de cada
operacin para poder estudiar qu sucede a cada paso. Analicemos,
entonces, el factorial(3) mediante la pila de evaluacin:

Instruccin

Contexto
de factorial

Resultado

factorial(3)

n3

if n == 0:

n3

Instruccin

Contexto
de factorial

Resultado

f = factorial (n-1)

n3

Se suspende el clculo. Se
llama afactorial(2)

factorial(2)

n2/n3

if n == 0:

n2/n3

f = factorial (n-1)

n2/n3

Se suspende el clculo. Se
llama afactorial(1)

factorial(1)

n1/n2/n3

if n == 0:

n1/n2/n3

f = factorial (n-1)

n1/n2/n3

Se suspende el clculo. Se
llama afactorial(0)

factorial(0)

n0/n1/n
2/n3

if n == 0:

n0/n1/n
2/n3

Instruccin

Contexto
de factorial

Resultado

r=1

n0r1/n
1 / n 2/ n 3

En factorial(0): return r

n1f1

n2/n3

r=n*f

n1f1r1/n
2 /n 3

En factorial(1): return r

n2f1

n3

r=n*f

n2f1r2/n
3

En factorial(2): return r

n3f2

En factorial(3): return r

n3f2

r=n*f

n3f2r6

En factorial(1): f = factorial
(n-1)

En factorial(2): f = factorial
(n-1)

Instruccin

Contexto
de factorial

Resultado

return r

Pila vaca

Devuelve el valor 6

18.6. Algoritmos
recursivos y algoritmos
iterativos
Llamaremos algoritmos recursivos a aquellos que realizan llamadas
recursivas para llegar al resultado, yalgoritmos iterativos a aquellos que
llegan a un resultado a travs de una iteracin mediante un ciclo definido
o indefinido.
Todo algoritmo recursivo puede expresarse como iterativo y viceversa.
Sin embargo, segn las condiciones del problema a resolver podr ser
preferible utilizar la solucin recursiva o la iterativa.

Una posible implementacin iterativa de la funcin factorial vista


anteriormente sera:
def factorial(n):
""" Precondicin: n entero >=0
Devuelve: n! """

fact = 1
for num in xrange(n, 1, -1):
fact *= num
return fact

Se puede ver que en este caso no es necesario incluir un caso base, ya


que el mismo ciclo incluye una condicin de corte, pero que s es
necesario incluir un acumulador, que en el caso recursivo no era
necesario.
Por otro lado, si hiciramos el seguimiento de esta funcin, como se hizo
para la versin recursiva, veramos que se trata de una nica pila, en la
cual se van modificando los valores de num y fact.
Es por esto que las versiones recursivas de los algoritmos, en general,
utilizan ms memoria (la pila del estado de las funciones se guarda en
memoria) pero suelen ser ms elegantes.

18.7. Un ejemplo de
recursividad elegante

Consideremos ahora otro problema que puede ser resuelto de forma


elegante mediante un algoritmo recursivo.
La funcin potencia(b,n), vista en unidades anteriores,
realizaba n iteraciones para poder obtener el valor de b^n. Sin embargo,
es posible optimizarla teniendo en cuenta que:

b^n = b^(n/2) b^(n/2) si n es par.

b^n = b^(n1)/2 b^(n1)/2 b si n es impar.

Antes de programar cualquier funcin recursiva es necesario decidir cul


ser el caso base y cul el caso recursivo. Para esta funcin,
tomaremos n = 0 como el caso base, en el que devolveremos 1; y el caso
recursivo tendr dos partes, correspondientes a los dos posibles grupos
de valores de n.
def potencia(b,n):
""" Precondicin: n debe ser mayor o igual que cero.
Devuelve: b\^n. """

# Caso base
if n <= 0:
return 1

# n par
if n % 2 == 0:
pot = potencia(b, n/2)
return pot * pot
# n impar

else:
pot = potencia(b, (n-1)/2)
return pot * pot * b

El uso de la variable pot en este caso no es optativo, ya que es una de las


ventajas principales de esta implementacin: se aprovecha el resultado
calculado en lugar de tener que calcularlo dos veces. Vemos que este
cdigo funciona correctamente:
>>> potencia(2,10)
1024
>>> potencia(3,3)
27
>>> potencia(5,0)
1

El orden de las llamadas, haciendo un seguimiento simplificado de la


funcin ser:
potencia(2,10)
pot = potencia(2,5)

# b 2 n 10

pot = potencia(2,2)

#b2n5

pot = potencia(2,1)

#b2n2

pot = potencia(2,0) # b 2 n 1
return 1
return 1 * 1 * 2
return 2 * 2
return 4 * 4 * 2
return 32 * 32

#b2n0
# b 2 n 1 pot 1
# b 2 n 2 pot 2
# b 2 n 5 pot 4
# b 2 n 10 pot 32

Se puede ver, entonces, que para calcular 2^10 se realizaron 5 llamadas


a potencia, mientras que en la implementacin ms sencilla se
realizaban 10 iteraciones. Y esta optimizacin ser cada vez ms
importante a medida que aumenta n, por ejemplo, para n = 100 se
realizarn 8 llamadas recursivas, para n = 1000, 11 llamadas.
Para transformar este algoritmo recursivo en un algoritmo iterativo, es
necesario simular la pila de llamadas a funciones mediante una pila que
almacene los valores que sean necesarios. En este caso, lo que
apilaremos ser si el valor de n es par o no.
def potencia(b,n):
""" Precondicin: n debe ser mayor o igual que cero.
Devuelve: b^n. """

pila = []
while n > 0:
if n % 2 == 0:
pila.append(True)
n /= 2
else:
pila.append(False)
n = (n-1)/2

pot = 1
while pila:
es_par = pila.pop()
if es_par:

pot = pot * pot


else:
pot = pot * pot * b

return pot

Como se puede ver, este cdigo es mucho ms complejo que la versin


recursiva, esto se debe a que utilizando recursividad el uso de la pila de
llamadas a funciones oculta el proceso de apilado y desapilado y permite
concentrarse en la parte importante del algoritmo.

18.8. Un ejemplo de
recursividad poco eficiente
Del ejemplo anterior se podra deducir que siempre es mejor utilizar
algoritmos recursivos, sin embargo - como ya se dijo - cada situacin
debe ser analizada por separado.
Un ejemplo clsico en el cual la recursividad tiene un resultado muy poco
eficiente es el de los nmeros de fibonacci. La sucesin de fibonacci est
definida por la siguiente relacin:
fib(0) = 0

fib(1) = 1

fib(n) = fib(n-1) + fib(n-2)

Los primeros nmeros de esta sucesin son: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34,


55.
Dada la definicin recursiva de la sucesin, puede resultar muy tentador
escribir una funcin que calcule en valor de fib(n) de la siguiente forma:
def fib(n):
""" Precondicin: n debe ser >= 0.
Devuelve: el nmero de fibonacci nmero n. """
if n == 0 or n == 1:
return n
return fib(n-1) + fib(n-2)

Sin embargo, si bien es muy sencillo y elegante, este cdigo es


extremadamente poco eficiente. Ya que para calcular fib(n-1) es
necesario calcular fib(n-2), que luego volver a ser calculado para
obtener el valor de fib(n).
Por ejemplo, una simple llamada a fib(5), generara recursivamente todas
las llamadas ilustradas en la Figura 18.3. Puede verse que muchas de
estas llamadas estn repetidas, generando un total de 15llamadas a la
funcin fib, slo para devolver el nmero 5.

Figura 18.1 rbol de llamadas para fib(5)


En este caso, ser mucho ms conveniente utilizar una versin iterativa,
que vaya almacenando los valores de las dos variables anteriores a
medida que los va calculando.
def fib(n):
""" Precondicin: n debe ser >= 0.
Devuelve: el nmero de fibonacci nmero n. """
if n == 0 or n == 1:
return n

ant2 = 0
ant1 = 1
for i in xrange(2, n+1):
fibn = ant1 + ant2
ant2 = ant1
ant1 = fibn

return fibn

Vemos que el caso base es el mismo para ambos algoritmos, pero que
en el caso iterativo se calcula el nmero de Fibonacci de forma
incremental, de modo que para obtener el valor de fib(n) se harn n
1 iteraciones.

ADVERTENCIAEn definitiva, vemos que un algoritmo recursivo no es


mejor que uno iterativo, ni viceversa. En cada situacin ser conveniente
analizar cul algoritmo provee la solucin al problema de forma ms clara
y eficiente.

18.9. Limitaciones
Si creamos una funcin sin caso base, obtendremos el equivalente
recursivo de un bucle infinito. Sin embargo, como cada llamada recursiva
agrega un elemento a la pila de llamadas a funciones y la memoria de
nuestras computadoras no es infinita, el ciclo deber terminarse cuando
se agote la memoria disponible.
En particular, en Python, para evitar que la memoria se termine, la pila de
ejecucin de funciones tiene un lmite. Es decir, que si se ejecuta un
cdigo como el que sigue:
def inutil(n):
return inutil(n-1)

Se obtendr un resultado como el siguiente:

>>> inutil(1)
File "<stdin>", line 2, in inutil
File "<stdin>", line 2, in inutil
(...)
File "<stdin>", line 2, in inutil
RuntimeError: maximum recursion depth exceeded

El lmite por omisin es de 1000 llamadas recursivas. Es posible


modificar el tamao mximo de la pila de recursin mediante la
instruccin sys.setrecursionlimit(n). Sin embargo, si se est alcanzando
este lmite suele ser una buena idea pensar si realmente el algoritmo
recursivo es el que mejor resuelve el problema.
NOTAExisten algunos lenguajes funcionales, como Haskell, ML, o
Scheme, en los cuales la recursividad es la nica forma de realizar un
ciclo. Es decir, no existen construcciones while ni for.
Estos lenguajes cuentan con una optimizacin especial,
llamada optimizacin de recursin por cola(tail recursion optimization),
que permite que cuando una funcin realiza su llamada recursiva
comoltima accin antes de terminar, no se apile el estado de la funcin
innecesariamente, evitando el consumo adicional de memoria
mencionado anteriormente.
La funcin factorial vista en esta unidad es un ejemplo de recursin por
cola cuya ejecucin puede ser optimizada por el compilador o intrprete
del lenguaje.

18.11. Ejercicios
Ejercicio 18.11.1. Escribir una funcin que reciba un nmero positivo n y
devuelva la cantidad de dgitos que tiene.
Ejercicio 18.11.2. Escribir una funcin que simule el siguiente
experimento: Se tiene una rata en una jaula con 3 caminos, entre los
cuales elige al azar (cada uno tiene la misma probabilidad), si elige el 1
luego de 3 minutos vuelve a la jaula, si elige el 2 luego de 5 minutos
vuelve a la jaula, en el caso de elegir el 3 luego de 7 minutos sale de la
jaula. La rata no aprende, siempre elige entre los 3 caminos con la misma
probabilidad, pero quiere su libertad, por lo que recorrer los caminos
hasta salir de la jaula.
La funcin debe devolver el tiempo que tarda la rata en salir de la jaula.
Ejercicio 18.11.3. Escribir una funcin que reciba 2 enteros n y b y
devuelva True si n es potencia de b. Ejemplos:
>>> es_potencia(8,2)
True
>>> es_potencia(64,4)
True
>>> es_potencia(70,10)
False

Ejercicio 18.11.4. Escribir una funcion recursiva que reciba como


parmetros dos strings a y b, y devuelva una lista con las posiciones en
donde se encuentra b dentro de a. Ejemplo:
>>> posiciones_de("Un tete a tete con Tete", "te")
[3, 5, 10, 12, 21]

Ejercicio 18.11.5. Escribir dos funciones mutualmente


recursivas par(n) e impar(n) que determinen la paridad del numero
natural dado, conociendo solo que:

1 es impar.

Si un nmero es impar, su antecesor es par; y viceversa.

Ejercicio 18.11.6. Escribir una funcin que calcule recursivamente el nsimo nmero triangular (el nmero 1 + 2 + 3 + ... + n).
Ejercicio 18.11.7. Escribir una funcin que calcule recursivamente
cuntos elementos hay en una pila, suponiendo que la pila slo tiene los
mtodos apilar y desapilar, y no altere el contenido de la pila.
Implementaras esta funcin para un programa real? Por qu?
Ejercicio 18.11.8. Escribir una funcion recursiva que encuentre el mayor
elemento de una lista.
Ejercicio 18.11.9. Escribir una funcin recursiva para replicar los
elementos de una lista una cantidad nde veces. Por ejemplo, replicar ([1,
3, 3, 7], 2) = ([1, 1, 3, 3, 3, 3, 7, 7])

http://librosweb.es/libro/algoritmos_python/capitulo_18/algoritmos_recursivos_y
_algoritmos_iterativos.html

S-ar putea să vă placă și