Memoização: Armazenando Resultados Recursivos em Cache
Aplique @functools.lru_cache e dicionários de memoização manuais a Fibonacci e climbing-stairs para eliminar recomputações exponenciais.
Memoização: Armazenando Resultados Recursivos em Cache é uma aula grátis de Coding Interview Prep no CoddyKit. Esta é a aula 4 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de Coding Interview Prep, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de Coding Interview Prep inclui 4 aulas no total.
O Problema da Recursão Redundante
O Fibonacci recursivo ingênuo calcula os mesmos valores repetidamente. fib(5) chama fib(4) e fib(3); fib(4) chama fib(3) e fib(2) — portanto, fib(3) é calculado duas vezes. Essa redundância cresce exponencialmente: fib(40) faz mais de um bilhão de chamadas de função. A memoização resolve isso armazenando cada resultado na primeira vez em que ele é calculado, para que as chamadas subsequentes o recuperem em O(1), em vez de recalculá-lo.
# 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=40Memoização Manual com um Dicionário
Adicione um dicionário memo como parâmetro (ou use um fechamento). Antes de calcular, verifique se a resposta já está em memo. Se estiver, retorne-a imediatamente. Caso contrário, calcule-a, armazene-a em memo e retorne-a. Agora, cada subproblema distinto é calculado exatamente uma vez, transformando O(2^n) em tempo O(n) e espaço O(n) para o dicionário memo, além de O(n) de espaço para a pilha.
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 fornece @functools.lru_cache(maxsize=None) (também disponível como @functools.cache no Python 3.9 ou posterior) para automatizar a memoização. Adicionar esse decorador acima de uma função armazena todas as chamadas com base em seus argumentos. maxsize=None significa que o tamanho do armazenamento temporário é ilimitado — cada combinação distinta de argumentos é armazenada. Isso transforma qualquer função recursiva em uma versão com memoização usando uma única linha 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=...)Subida de Escadas (LeetCode 70)
LeetCode 70, 'Subida de Escadas': você pode subir 1 ou 2 degraus por vez. Quantas maneiras existem de chegar ao degrau n? Isso é Fibonacci disfarçado: ways(n) = ways(n-1) + ways(n-2). Casos-base: ways(0) = 1 (uma maneira de permanecer no chão), ways(1) = 1. Com memoização, o tempo é O(n) e o espaço é 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,21Troco (LeetCode 322)
LeetCode 322, 'Troco': dadas as denominações e um valor-alvo, encontre o número mínimo de moedas. Recursão de cima para baixo com memoização: dp(amount) = 1 + min(dp(amount - coin)) para cada moeda válida. O caso-base é: dp(0) = 0. Armazene temporariamente cada subvalor. Se um subvalor for impossível, retorne infinito. A memoização transforma a força bruta exponencial em tempo O(valor-alvo × quantidade de moedas).
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)) # -1Divisão de Palavras (LeetCode 139) com Memoização
LeetCode 139, 'Divisão de Palavras': determine se uma cadeia de caracteres pode ser segmentada em palavras do dicionário. A recursão de cima para baixo: can_break(s, start) tenta cada prefixo s[start:end]; se ele estiver no dicionário e can_break(s, end) for verdadeiro, retorne verdadeiro. Sem memoização, isso é O(2^n); com memoização (armazenando temporariamente cada índice inicial), torna-se O(n² × L), em que L é o comprimento máximo de uma palavra.
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'])) # FalseMemoização versus Tabulação
Memoização (de cima para baixo) começa com o problema original e armazena as respostas à medida que elas são descobertas recursivamente. Ela resolve apenas os subproblemas realmente necessários. A tabulação (de baixo para cima) preenche previamente uma tabela, partindo de subproblemas pequenos até chegar aos grandes, e resolve todos os subproblemas. A memoização é mais fácil de derivar de uma solução recursiva; a tabulação evita limitações de profundidade da recursão e a sobrecarga de chamadas de função.
# 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 limitOtimização de Espaço: Variáveis Deslizantes
Muitos problemas de DP que a recursão com memoização resolve usando espaço O(n) podem ser ainda mais otimizados para espaço O(1) quando apenas um número fixo de respostas de subproblemas anteriores é necessário. Para Fibonacci, somente os dois últimos valores importam. Para a subida de escadas, ocorre o mesmo. Duas variáveis deslizantes substituem todo o dicionário ou a tabela de memoização.
# 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)) # 89lru_cache versus Fechamento versus Dicionário Global
Há três maneiras de implementar a memoização manualmente. Um dicionário global é simples, mas polui o escopo do módulo. Um fechamento encapsula o armazenamento temporário dentro da função, evitando vazamentos, mas exige uma função envoltória. @lru_cache é a opção mais limpa — um único decorador substitui todo o código repetitivo. Em uma entrevista, comece com @lru_cache, a menos que o entrevistador peça especificamente uma implementação 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 832040Quando a Memoização Não Ajuda
A memoização só acelera problemas com subproblemas sobrepostos — casos em que o mesmo subproblema é calculado várias vezes. Se cada subproblema for único (como em um percurso simples de árvore, em que cada nó é visitado exatamente uma vez), a memoização adicionará sobrecarga sem benefício. Além disso, a memoização não pode corrigir problemas em que a árvore recursiva é exponencial no número de subproblemas distintos, e não no reaproveitamento — esses problemas exigem um 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')Resumo: Lista de Verificação da Memoização
Aplique a memoização quando: você tiver uma solução recursiva correta, mas lenta devido a recálculos redundantes; a função tiver um número pequeno de combinações distintas de argumentos; e o valor retornado depender apenas dos argumentos (função pura — sem efeitos colaterais e sem estado global). Verifique o espaço de estados dos subproblemas: se houver no máximo O(n) ou O(n²) estados distintos, a memoização transforma o tempo exponencial em polinomial.
Verificação Rápida
Verifique sua compreensão dos conceitos de Estruturas de Dados e Algoritmos — Preparação para Entrevistas de Programação desta lição.
Recapitulação da Lição
Nesta lição, você aprendeu: a memoização armazena os resultados dos subproblemas para evitar recálculos, transformando a recursão exponencial em tempo polinomial, @functools.lru_cache é a ferramenta idiomática do Python e requer apenas uma linha e a memoização (de cima para baixo) e a tabulação (de baixo para cima) são os dois estilos de DP — a memoização é mais fácil de derivar, enquanto a tabulação evita problemas de profundidade da pilha. Parabéns — você concluiu os módulos de recursão e mapas de dispersão!
Perguntas Frequentes
A aula “Memoização: Armazenando Resultados Recursivos em Cache” é grátis?
Sim — o texto completo de “Memoização: Armazenando Resultados Recursivos em Cache” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de Coding Interview Prep, atualize para CoddyKit PRO. O curso de Coding Interview Prep inclui 4 aulas no total.
O que vou aprender em “Memoização: Armazenando Resultados Recursivos em Cache”?
Aplique @functools.lru_cache e dicionários de memoização manuais a Fibonacci e climbing-stairs para eliminar recomputações exponenciais. Você pratica Coding Interview Prep com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.
Preciso ter experiência prévia para começar Coding Interview Prep?
Nenhuma experiência prévia é necessária. Coding Interview Prep no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 4 de 4.
Quanto tempo leva a aula “Memoização: Armazenando Resultados Recursivos em Cache”?
A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.
Posso escrever e executar código nesta aula de Coding Interview Prep?
Sim. Cada aula de Coding Interview Prep inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.
Todas as aulas deste curso
- Estrutura da Recursão: Caso Base, Confiança, Construção
- Visualizando a Pilha de Chamadas
- Compromissos entre Recursão e Iteração
- Memoização: Armazenando Resultados Recursivos em Cache