Una función recursiva
es aquella que se llama a sí mismo, normalmente, un número
finito de veces. Por su lado, la iteratividad
ejecuta unas determinadas acciones
un determinado numero de veces tambien, pero de forma secuencial.
El contraste esta cuando queremos ejecutar una determinada acción una cantidad de veces, que bien podríamos hacerlo con recursividad o iteratividad, aunque escoger la mejor opción es la interrogante aquí.
Cuando usamos recursividad estamos haciendo caso al método "divide y vencerás", ya que
dividimos el problema en partes mas pequeñas para su resolución. Cuando se emplea una
función recursiva básicamente se divide la función en dos partes: 1) paso recursivo
, es la parte que hace la llamada recursiva, por tanto también se llama llamada recursiva
, 2) caso base
que igualmente podrían ser más de uno, es aquí cuando el método en verdad devuelve un resultado, por lo que se podría decir que es lo único que se resuelve en una función recursividad.
A continuación veremos un ejemplo de función recursiva para buscar el factorial de un número entero positivo. El factorial de un numero se define como
n * (n-1) (n-2) * ... * 1
Teniendo en cuenta que 1! y 0! es igual a 1, primero presentaré la solución con una función iterativa:
#!/usr/bin/env python # Función factorial usando un método iterativo def factorial(n): retorno = 1 for x in range(1,n+1): retorno *=x return retorno def main(): for x in range(11): print "%d! = %d" % (x,factorial(x)) if __name__ == "__main__": main()
Ahora veamos el método recursivo que en efecto produce el mismo resultado
#/usr/bin/env python # Factorial función recursiva def factorial(n): if n <= 1: return 1 else: return n * factorial(n-1) def main(): for x in range(11): print "%d! = %d" % (x, factorial(x)) if __name__ == "__main__": main()
En este caso n =< 1 es lo que llamamos caso base
y n * factorial(n-1)
es el paso recursivo
o llamada recursiva
.
La secuencia de llamadas recursivas y retornos para el factorial de 5 va como se muestra en lo adelante.
+----+ +----+ valor final = 120 | 5! | | 5! | +----+ +-^--+ | | | | V | se devuelve 5! = 5 * 24 = 120 +-------+ +--------+ | 5 * 4!| | 5 * 4! | +-------+ +-----^--+ | | | | V | se devuelve 4! = 4 * 6 = 24 +-------+ +---------+ | 4 * 3!| | 4 * 3! | +-------+ +------^--+ | |
| | V | se devuelve 3! = 3 * 2 = 6 +-------+ +--------+ | 3 * 2!| | 3 * 2! | +-------+ +-----^--+ | | | | V | se devuelve 2! = 2 * 1 = 2 +-------+ +--------+ | 2 * 1 | | 2 * 1! | +-------+ +-----^--+ | | | | V | se devuelve 1 +------+ +-------+ | 1 | | 1 | +------+ +-------+ Secuencia de llamadas recursivas Valores devueltos de cada llamada
Ojo, que la secuencia de las llamadas recursivas esta ilustrada de arriba para abajo y los valores devueltos de abajo para arriba.
Ejecutamos ambos scripts (uno a uno claro) precedido por el comando time
para medir el tiempo de ejecución de ambos scripts, time esta por defecto creo en todas las distros GNU/Linux - sí, supongo que usas Linux, no? no?.
time python factorial.py
Debo decir, que estoy usando Python 2.7, veamos ahora la salida de ambos programas.
Función recursiva
0! = 1 1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 6! = 720 7! = 5040 8! = 40320 9! = 362880 10! = 3628800 real 0m0.025s user 0m0.017s sys 0m0.007s
Función iterativa
0! = 1 1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 6! = 720 7! = 5040 8! = 40320 9! = 362880 10! = 3628800 real 0m0.027s user 0m0.020s sys 0m0.003s
Como pueden ver la diferencia es muy poca, pero aún así las hay. Hay muchas cosas que se pueden decir al respecto ahora, pero mejor veamos otro ejemplo más.
Ahora será con la sucesión de fibonacci, veamos la función recursiva primero.
#/usr/bin/env python # Función fibonacci recursiva def fibonacci(n): if n == 0 or n == 1: return n else: return fibonacci(n-1) + fibonacci(n-2) def main(): for x in range(11): print "Fibonnaci de %d es %d" % (x, fibonacci(x)) if __name__ == "__main__": main()
Aquí el caso base
es n == 0 or n == 1, ya que es cuando sabemos con seguridad el valor que debemos devolver, el cual es el mismo parámetro.
Graficando el conjunto de llamadas sucesivas para fibonacci(3) tenemos:
+---------------+ | fibonacci(3) | +---------------+ | |Más fácil de entender así no?
V -----------------------^--------------------------- return fibonacci(2) + fibonacci(1) -------------- -------------- / \ / \ / \ ------------------^------------------ ------------- return fibonacci(1) + fibonacci(0) return 1 ------------ ------------
| | | | | | -----^----- -------^------ return 1 return 0
Ahora veamos la salida de ambos programas, tanto el que tiene usa la función recursiva como el que usa la función iterativa.
Función recursiva
Fibonnaci de 0 es 0 Fibonnaci de 1 es 1 Fibonnaci de 2 es 1 Fibonnaci de 3 es 2 Fibonnaci de 4 es 3 Fibonnaci de 5 es 5 Fibonnaci de 6 es 8 Fibonnaci de 7 es 13 Fibonnaci de 8 es 21 Fibonnaci de 9 es 34 Fibonnaci de 10 es 55 real 0m0.028s user 0m0.023s sys 0m0.003s
Función iterativa Fibonacci de 0 es 0 Fibonacci de 1 es 1 Fibonacci de 2 es 1 Fibonacci de 3 es 2 Fibonacci de 4 es 3 Fibonacci de 5 es 5 Fibonacci de 6 es 8 Fibonacci de 7 es 13 Fibonacci de 8 es 21 Fibonacci de 9 es 34 Fibonacci de 10 es 55
real 0m0.024s user 0m0.017s sys 0m0.003s
Ahh, pues ya tenemos dos ejemplos con los cuales poder ir haciendo conclusiones, como podemos ver en algunos casos la recursividad es más rápida y en otros casos la iteratividad es más rápida. Pero se puede ver claramente que el problema esta cuando hacemos demasiadas llamadas al mismo método, se consume mucha memoria, es el problema aquí con la función recursiva fibonacci, hacemos bastantes llamadas al mismo método, de hecho personas con más conocimientos que el wannabe que escribe esto aconsejan a no usar recursividad en casos así, porque se carga bastante el sistema.
Así que donde se hacen bastante llamadas a un mismo método el resultado podría no ser muy óptimo, en otros casos si, por eso vemos que con el factorial el método recursivo es en efecto más rápido.
La recursividad así como la iteración se basan en una instrucción de control: la iteración utiliza una instrucción de repetición (for, while o do...while), mientras que la recursividad utiliza una instrucción de selección (if, if...else o switch), de modo que ambas implican repetición, sólo que difieren en el como lo hacen.
Del libro Java Como programar de Deitel nos aconsejan lo siguiente:
Cualquier problema que se pueda resolver mediante la recursividad, se puede también mediante la iteración(sin recursividad). Por lo general, se prefiere un método recursivo a uno iterativo cuando el primero refleja con más naturalidad el problema, y se produce un programa más fácil de entender y de depurar. A menudo, puede implementarse un método recursivo con menos lineas de código. Otra razón por la que es preferible elegir un método recursivo es que uno iterativo podría no ser aparente.
Evite usar la recursividad en situaciones en las que se requiera un alto rendimiento. Las llamadas recursivas requieren tiempo y consumen memoria adicional.
Dicho esos sabios consejos, podemos sacar de ahí que un método recursivo implica bien menos código pero a veces no es aconsejable por cuestiones de rendimiento, aunque ahí dice que podría ser además más obvio y legible yo diría que más podaría confundir a veces, pero bueno básicamente es saber donde utilizarlos son muy buenos recursos, solo toca analizar bien, porque si nos afecta bastante el rendimiento pues usar un método iterativo es la mejor opción.
Un último ejemplo, usamos una función recursiva y una iterativa en diferentes programas, para calcular la potencia de un número.
Primero veamos la solución iterativa:
#!/usr/bin/env python # Exponenciacion iterativo def exponenciacion(base, exponente): retorno = base for x in range(exponente-1): retorno = retorno*base return retorno def main(): for x in range(1,10): for z in range(1,10): print "%{0}^%{1} = %{2}".format(x, z, exponenciacion(x, z)) print "" if __name__ == "__main__": main()
Fácil, ahora veamos la solución recursiva:
#!/usr/bin/env python # Exponenciacion recursivo def exponenciacion(base, exponente): if exponente == 1: return base else: return base * exponenciacion(base, exponente-1) def main(): for x in range(1,10): for z in range(1,10): print "%{0}^%{1} = %{2}".format(x, z, exponenciacion(x, z)) print "" if __name__ == "__main__": main()
Ahora es el momento de ver el tiempo de ejecución de ambos, la salida del comando time
, no mostraré la salida de los programas porque es algo amplía como se puede deducir del código.
Tiempo ejecución función recursiva:
real 0m0.025s user 0m0.020s sys 0m0.003s
Tiempo ejecución función iterativa:
real 0m0.027s user 0m0.020s sys 0m0.003s
Nuevamente aquí la recursividad ganó en cuanto a rapidez, por la misma razón que antes, no tiene muchas llamadas al mismo método, en casos así funciona fluido. El marcador final en cuando a rapidez: recursividad 2 - iteratividad 1!