0Pricing
Coding Interview Prep · Lección

Memoización: almacenamiento en caché de resultados recursivos

Aplique @functools.lru_cache y diccionarios memo manuales a Fibonacci y climbing-stairs para eliminar recomputaciones exponenciales.

Memoización: almacenamiento en caché de resultados recursivos es una lección gratuita de Coding Interview Prep en CoddyKit. Esta es la lección 4 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de Coding Interview Prep, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de Coding Interview Prep incluye 4 lecciones en total.

El problema de la recursividad redundante

El Fibonacci recursivo ingenuo calcula los mismos valores repetidamente. fib(5) llama a fib(4) y fib(3); fib(4) llama a fib(3) y fib(2), por lo que fib(3) se calcula dos veces. Esta redundancia crece de forma exponencial: fib(40) realiza más de mil millones de llamadas a funciones. La memoización resuelve este problema almacenando cada resultado la primera vez que se calcula, de modo que las llamadas posteriores lo recuperan en O(1) en lugar de volver a calcularlo.

# Count calls without memoisation
call_count = [0]

def fib_plain(n):
    call_count[0] += 1
    if n <= 1: return n
    return fib_plain(n-1) + fib_plain(n-2)

fib_plain(20)
print(f'fib(20) without memo: {call_count[0]:,} calls')
# ~21,891 calls for n=20; ~1 billion for n=40

Memoización manual con un diccionario

Añada un diccionario memo como parámetro (o utilice una clausura). Antes de realizar el cálculo, compruebe si la respuesta ya está en memo. Si es así, devuélvala inmediatamente. Si no, calcúlela, almacénela en memo y devuélvala. Ahora cada subproblema único se calcula exactamente una vez, lo que transforma O(2^n) en un tiempo de O(n) y un espacio de O(n) para el diccionario memo, además de un espacio de pila de O(n).

def fib_memo(n, memo={}):
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
    return memo[n]

print(fib_memo(10))   # 55
print(fib_memo(50))   # 12586269025
print(fib_memo(100))  # huge number — still fast!

Decorador functools.lru_cache

Python proporciona @functools.lru_cache(maxsize=None) (también disponible como @functools.cache en Python 3.9+) para automatizar la memoización. Al añadir este decorador encima de una función, se almacenan en caché todas las llamadas según sus argumentos. maxsize=None significa que el tamaño de la caché es ilimitado: se almacena en caché cada combinación única de argumentos. Esto convierte cualquier función recursiva en una versión memoizada con una sola línea de código.

import functools

@functools.lru_cache(maxsize=None)
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

print(fib(50))   # 12586269025
print(fib(100))  # 354224848179261915075
print(fib.cache_info())  # CacheInfo(hits=..., misses=..., maxsize=None, currsize=...)

Subir escaleras (LeetCode 70)

LeetCode 70 «Climbing Stairs»: puede subir 1 o 2 escalones cada vez. ¿Cuántas formas hay de llegar al escalón n? En realidad, se trata de Fibonacci: ways(n) = ways(n-1) + ways(n-2). Casos base: ways(0) = 1 (hay una forma de permanecer en el suelo) y ways(1) = 1. Con memoización, el tiempo de ejecución es O(n) y el espacio es O(n).

import functools

@functools.lru_cache(maxsize=None)
def climbStairs(n):
    if n <= 1:
        return 1
    return climbStairs(n-1) + climbStairs(n-2)

for i in range(1, 8):
    print(f'climbStairs({i}) = {climbStairs(i)}')
# 1,2,3,5,8,13,21

Cambio de monedas (LeetCode 322)

LeetCode 322 «Coin Change»: dadas unas denominaciones y una cantidad objetivo, encuentre el número mínimo de monedas. Recursividad memoizada de arriba abajo: dp(amount) = 1 + min(dp(amount - coin)) para cada moneda válida. El caso base es dp(0) = 0. Almacene en caché cada subcantidad. Si una subcantidad es imposible, devuelva infinito. La memoización transforma la fuerza bruta exponencial en un tiempo de O(amount × len(coins)).

import functools

def coinChange(coins, amount):
    @functools.lru_cache(maxsize=None)
    def dp(rem):
        if rem == 0:
            return 0
        if rem < 0:
            return float('inf')
        return 1 + min(dp(rem - c) for c in coins)

    result = dp(amount)
    return result if result != float('inf') else -1

print(coinChange([1, 5, 11], 15))  # 3 (5+5+5)
print(coinChange([1, 2, 5], 11))   # 3 (5+5+1)
print(coinChange([2], 3))          # -1

Word Break (LeetCode 139) con memoización

LeetCode 139 «Word Break»: determine si una cadena puede segmentarse en palabras del diccionario. La recursividad de arriba abajo: can_break(s, start) prueba cada prefijo s[start:end]; si está en el diccionario y can_break(s, end) es true, devuelve true. Sin memoización, esto es O(2^n); con memoización (almacenando en caché cada índice de inicio), pasa a ser O(n² × L), donde L es la longitud máxima de palabra.

import functools

def wordBreak(s, wordDict):
    word_set = set(wordDict)

    @functools.lru_cache(maxsize=None)
    def can_break(start):
        if start == len(s):
            return True
        for end in range(start + 1, len(s) + 1):
            if s[start:end] in word_set and can_break(end):
                return True
        return False

    return can_break(0)

print(wordBreak('leetcode', ['leet', 'code']))    # True
print(wordBreak('applepenapple', ['apple','pen'])) # True
print(wordBreak('catsandog', ['cats','dog','sand','and','cat']))  # False

Memoización frente a tabulación

La memoización (de arriba abajo) comienza con el problema original y almacena en caché las respuestas a medida que se descubren recursivamente. Solo resuelve los subproblemas que realmente se necesitan. La tabulación (de abajo arriba) rellena previamente una tabla desde los subproblemas pequeños hasta los grandes, resolviendo todos los subproblemas. La memoización es más fácil de derivar a partir de una solución recursiva; la tabulación evita las limitaciones de profundidad de la recursividad y la sobrecarga de las llamadas a funciones.

# Memoisation (top-down)
import functools
@functools.lru_cache(maxsize=None)
def fib_td(n):
    if n <= 1: return n
    return fib_td(n-1) + fib_td(n-2)

# Tabulation (bottom-up)
def fib_bu(n):
    if n <= 1: return n
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]

print(fib_td(20), fib_bu(20))   # 6765 6765
# Both O(n) time; fib_bu avoids recursion limit

Optimización del espacio: variables deslizantes

Muchos problemas de DP que la recursividad memoizada resuelve usando un espacio O(n) pueden optimizarse aún más hasta un espacio O(1) cuando solo se necesita un número fijo de respuestas de subproblemas anteriores. En Fibonacci, solo importan los dos últimos valores. En el problema de subir escaleras ocurre lo mismo. Dos variables deslizantes sustituyen al diccionario memo o a la tabla completos.

# Fibonacci with O(1) space
def fib_o1(n):
    if n <= 1:
        return n
    prev2, prev1 = 0, 1
    for _ in range(2, n + 1):
        prev2, prev1 = prev1, prev2 + prev1
    return prev1

for i in range(8):
    print(f'fib({i})={fib_o1(i)}', end='  ')
print()

# Climbing stairs O(1) space
def climbStairs_o1(n):
    if n <= 1: return 1
    a, b = 1, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b
print(climbStairs_o1(10))  # 89

lru_cache frente a clausura y diccionario global

Hay tres formas de implementar manualmente la memoización. Un diccionario global es sencillo, pero contamina el ámbito del módulo. Una clausura encapsula la caché dentro de la función, evitando fugas, pero requiere un wrapper. @lru_cache es la opción más limpia: un decorador sustituye todo el código repetitivo. En una entrevista, comience con @lru_cache, a menos que el entrevistador le pida específicamente una implementación manual.

import functools

# 1. Global dict (messy)
memo_global = {}
def fib_global(n):
    if n in memo_global: return memo_global[n]
    if n <= 1: return n
    memo_global[n] = fib_global(n-1) + fib_global(n-2)
    return memo_global[n]

# 2. Closure (cleaner scope)
def make_fib():
    cache = {}
    def fib(n):
        if n in cache: return cache[n]
        if n <= 1: return n
        cache[n] = fib(n-1) + fib(n-2)
        return cache[n]
    return fib
fib_closure = make_fib()

# 3. lru_cache (best)
@functools.lru_cache(maxsize=None)
def fib_cached(n):
    if n <= 1: return n
    return fib_cached(n-1) + fib_cached(n-2)

print(fib_global(30), fib_closure(30), fib_cached(30))  # all 832040

Cuándo la memoización no ayuda

La memoización solo acelera los problemas con subproblemas superpuestos, es decir, los casos en los que el mismo subproblema se calcula varias veces. Si cada subproblema es único (como en un recorrido simple de árbol, donde cada nodo se visita exactamente una vez), la memoización añade sobrecarga sin aportar ningún beneficio. Además, la memoización no puede resolver problemas en los que el árbol de recursión es exponencial respecto al número de subproblemas distintos en lugar de serlo debido a la reutilización; esos problemas requieren un algoritmo completamente diferente.

# Memoisation DOES help: overlapping sub-problems (Fibonacci)
# fib(n) reuses fib(n-2), fib(n-3), etc.

# Memoisation does NOT help: distinct sub-problems (permutations)
# Each unique (remaining_elements, target) pair is truly distinct
# The exponential complexity comes from the state space itself

print('Memoisation: useful when SAME sub-problem recurs multiple times')
print('Not useful: when every sub-problem is unique to one recursive path')

Resumen: lista de comprobación de la memoización

Aplique memoización cuando: tenga una solución recursiva correcta pero lenta debido a cálculos repetidos; la función tenga un número reducido de combinaciones distintas de argumentos; y el valor devuelto dependa únicamente de los argumentos (una función pura, sin efectos secundarios ni estado global). Compruebe el espacio de estados de los subproblemas: si hay como máximo O(n) u O(n²) estados distintos, la memoización transforma el tiempo exponencial en polinómico.

Comprobación rápida

Compruebe su comprensión de los conceptos de Data Structures & Algorithms — Coding Interview Prep de esta lección.

Repaso de la lección

En esta lección ha aprendido que: la memoización almacena los resultados de los subproblemas para evitar volver a calcularlos, transformando la recursividad exponencial en tiempo polinómico; @functools.lru_cache es la herramienta idiomática de Python y solo requiere una línea; y la memoización (de arriba abajo) y la tabulación (de abajo arriba) son los dos estilos de DP: la memoización es más fácil de derivar y la tabulación evita los problemas de profundidad de la pila. ¡Enhorabuena! Ha completado los módulos de recursividad y mapas hash.

Preguntas frecuentes

¿La lección «Memoización: almacenamiento en caché de resultados recursivos» es gratis?

Sí — el texto completo de «Memoización: almacenamiento en caché de resultados recursivos» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de Coding Interview Prep, actualiza a CoddyKit PRO. El curso de Coding Interview Prep incluye 4 lecciones en total.

¿Qué aprenderé en «Memoización: almacenamiento en caché de resultados recursivos»?

Aplique @functools.lru_cache y diccionarios memo manuales a Fibonacci y climbing-stairs para eliminar recomputaciones exponenciales. Practicas Coding Interview Prep con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.

¿Necesito experiencia previa para empezar Coding Interview Prep?

No se requiere experiencia previa. Coding Interview Prep en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 4 de 4.

¿Cuánto tiempo toma la lección «Memoización: almacenamiento en caché de resultados recursivos»?

La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.

¿Puedo escribir y ejecutar código en esta lección de Coding Interview Prep?

Sí. Cada lección de Coding Interview Prep incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.

Todas las lecciones de este curso

  1. Marco de recursión: caso base, confianza y construcción
  2. Visualización de la pila de llamadas
  3. Compensaciones entre recursión e iteración
  4. Memoización: almacenamiento en caché de resultados recursivos
← Volver a Coding Interview Prep