La memoización es una técnica de optimización que evita recalcular resultados previamente obtenidos al almacenarlos en una estructura de datos. Esto reduce el número de llamadas a funciones recursivas y mejora el rendimiento al ahorrar tiempo de cálculo a costa de usar más memoria. La programación dinámica, de la cual la memoización es una técnica, es útil para problemas donde las soluciones a subproblemas se solapan y pueden construirse a partir de soluciones óptimas de subproblemas.
2. INTRODUCCIÓN
La memoria de nuestra computadora no es infinita, existe un máximo de funciones y cálculos
que podemos hacer. Incluso si no la usamos toda, gastarla excesivamente causará que
nuestras aplicaciones corran lento, con mucho lag o sencillamente briden una muy mala
experiencia a los usuarios.
Nuestro código puede parecer pequeño cuando utilizamos técnicas de programación
funcional como currying y recursividad. Pero no te dejes engañar. Así estemos llamando a la
misma función una y otra vez recursivamente, cada cálculo o llamado a esta función genera
nuevos “bloques” en la pila de ejecuciones que debe hacer el lenguaje de programación. Esto
afecta a la memoria del pc y puede estropear nuestra aplicación.
La buena noticia es que muy seguramente no tienes de qué preocuparte. Este “problema” no
será realmente un problema a menos que construyas aplicaciones muy, muy grandes (por
ejemplo, videojuegos en el navegador) donde la optimización de memoria es vital.
3. LA MEMOIZACIÓN
La memoización es una técnica muy útil para evitar
cálculos innecesarios en nuestro código. Guardamos
el resultado de nuestros cálculos cada vez que los
hacemos para no tener que repetirlos en el futuro.
En otras palabras, estamos ahorrando grandes
cantidades de tiempo a cambio de “mucho”
espacio de almacenamiento.
4. LA MEMOIZACIÓN
Memoización es una técnica de optimización que evita recalcular
resultados previamente obtenidos. Para esto, los resultados anteriores se
almacenan; y cuando se pide un resultado ya calculado, se retorna el valor
recordado, evitando recalcularlo.
La memoización se aplica a funciones que cumplan dos requisitos:
• Idempotentes. Es decir, funcion(n) siempre retornara el mismo valor
para un n dado.
• Sin efectos secundarios. La función no debe alterar el ambiente
externo. Por ejemplo, no puede cambiar variables globales, el
contenido de archivos o bases de datos, imprimir, etc.
5. LA MEMOIZACIÓN
La recursividad puede ser una manera elegante para resolver un problema
y muchos algoritmos se prestan para soluciones recursivas. Sin embargo,
los algoritmos recursivos pueden ser ineficientes en términos tanto de
tiempo como de espacio.
En el desafío de codificación para calcular recursivamente el factorial de
un número, te pedimos llamar la función varias veces con diferentes
valores.
Por ejemplo, llamemos los siguientes factoriales:
factorial(0);
factorial(2);
factorial(5);
factorial(10);
6. LA MEMOIZACIÓN
Estas serian todas las llamadas que hace la computadora al ejecutar esos 4
factoriales:
Observa que factorial(10) tiene que hacer 11 llamadas de la función, y 6 de ellas tienen
exactamente los mismos argumentos y valores de resultado que las llamadas anteriores de la
función realizadas durante factorial(5).
7. MEMOIZACIÓN DEL FACTORIAL
Podemos usar una técnica llamada memoización para ahorrar tiempo a la
computadora al hacer llamadas idénticas a la función.
Una memoización de la función factorial podría verse así:
Si n = 0, regresa 1
De lo contrario, si n está en el memo, regresa el valor del memo para n
De lo contrario,
Calcula (n - 1)! . (n)
Almacena el resultado en el memo
Regresa el resultado
Este algoritmo revisa el valor de entrada en el memo antes de hacer una
llamada recursiva potencialmente costosa. El memo debe ser una estructura
de datos con tiempos de búsqueda eficientes, tales como una tabla hash con
un tiempo de búsqueda de O(1)
8. LA MEMOIZACIÓN FACTORIAL
Con la memoización implementada, la computadora puede hacer menos
llamadas totales sobre llamadas repetidas a factorial():
La memoización hace un intercambio
entre el tiempo y el espacio. Mientras
la búsqueda sea eficiente y la función
sea llamada repetidamente, la
computadora puede ahorrar tiempo a
costa de usar memoria para almacenar
el memo.
9. MEMOIZACIÓN DE FIBONACCI
En el caso de la función factorial, un algoritmo solo se beneficia de la
optimización de la memoización cuando un programa hace llamadas
repetidas a la función durante su ejecución. Sin embargo, en algunos
casos, la memoización puede ahorrar tiempo incluso para una única
llamada a una función recursiva.
Veamos un ejemplo simple: el algoritmo para generar números de
Fibonacci.
La sucesión de Fibonacci es una serie famosa de números donde el
siguiente número en la sucesión es la suma de los 2 números anteriores.
Los dos primeros números de la sucesión están definidos como 0 y 1.
Después de eso, el siguiente número es 1 (a partir de 0+1) y el siguiente
número después de eso es 2 (a partir de 1 + 1), y así sucesivamente.
10. MEMOIZACIÓN DE FIBONACCI
Los primeros 10 números de Fibonacci:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34
Una función recursiva simple para generar el n-ésimo número de
Fibonacci se ve así:
Si n es 0 o 1, regresa n
De lo contrario, regresa fibonacci(n -1) + fibonacci(n - 2)
Este algoritmo es un ejemplo de recursividad múltiple ya que se llama a sí
mismo varias veces. Para entender las múltiples llamadas que realiza la
computadora, es útil visualizar las llamadas como un árbol.
11. MEMOIZACIÓN DE FIBONACCI
Cuando n=5, la computadora hace 15 llamadas:
Observa las múltiples llamadas a la
función fibonacci para los valores de
entrada de 3, 2, 1 y 0. A medida que las
entradas se hacen más grandes, esto se
vuelve cada vez más ineficiente. Una
llamada a fibonacci(30) da como
resultado que la computadora
llame fibonacci(2) más de medio
millón de veces.
12. MEMOIZACIÓN DE FIBONACCI
Aquí podemos usar la memoización para evitar que la computadora
recalcule un número de Fibonacci que ya está calculado.
La versión memoizada del algoritmo recursivo de Fibonacci se ve así:
Si n es 0 o 1, regresa n
De lo contrario, si n está en el memo, regresa el valor del memo para n
De lo contrario,
• Calcula fibonacci(n - 1) + fibonacci(n – 2)
• Almacena el resultado en el memo
• Regresa el resultado
13. MEMOIZACIÓN DE FIBONACCI
Para n=5, la computadora hace 9 llamadas:
La versión original del algoritmo
requirió 15 llamadas de la función, por
lo que la memoización eliminó 6
llamadas.
14. MEMOIZACIÓN DE FIBONACCI
Esta tabla muestra el número de llamadas requeridas desde n=5 hasta n=10:
El número total de llamadas a la función aumenta a una tasa exponencial
para el algoritmo original, pero a una tasa lineal mucho más lenta para el
algoritmo memoizado.
Sin embargo, el algoritmo memoizado requiere más espacio, suficiente
para que el memo almacene cada valor de respuesta de n.
15. PROGRAMACIÓN DINÁMICA
La memoización es una técnica de programación dinámica, una estrategia
de resolución de problemas que se utiliza en matemáticas y ciencias de la
computación.
La programación dinámica se puede utilizar cuando un problema tiene
una subestructura óptima y superposición de subproblemas. La
subestructura óptima significa que la solución óptima al problema se
puede crear a partir de soluciones óptimas de sus subproblemas. En otras
palabras, fib(5) puede resolverse con fib(4) y fib(3). La superposición de
subproblemas ocurre siempre que un subproblema se resuelve varias
veces, lo cual vimos cuando fib(5) hizo varias llamadas a las típicamente
recursivas fib(3) y fib(2).
La programación dinámica se puede utilizar para una serie de problemas e
involucra otras técnicas además de las que aprendimos aquí.