Mémoïsation : mettre en cache les résultats récursifs
Appliquez @functools.lru_cache et des dictionnaires de mémoïsation manuelle à Fibonacci et climbing-stairs pour éliminer les recalculs exponentiels.
Mémoïsation : mettre en cache les résultats récursifs est une leçon Coding Interview Prep gratuite sur CoddyKit. Ceci est la leçon 4 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage Coding Interview Prep, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours Coding Interview Prep comprend 4 leçons au total.
Le problème des appels récursifs redondants
La version récursive naïve de Fibonacci recalcule plusieurs fois les mêmes valeurs. fib(5) appelle fib(4) et fib(3) ; fib(4) appelle fib(3) et fib(2) — fib(3) est donc calculé deux fois. Cette redondance augmente de manière exponentielle : fib(40) effectue plus d’un milliard d’appels de fonction. La mémoïsation résout ce problème en stockant chaque résultat lors de son premier calcul, afin que les appels suivants le récupèrent en O(1) au lieu de le recalculer.
# 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=40Mémoïsation manuelle avec un dictionnaire
Ajoutez un dictionnaire memo comme paramètre (ou utilisez une fermeture). Avant d’effectuer le calcul, vérifiez si la réponse se trouve déjà dans le dictionnaire de mémo. Si c’est le cas, renvoyez-la immédiatement. Sinon, calculez-la, stockez-la dans le dictionnaire, puis renvoyez-la. Chaque sous-problème distinct est alors calculé exactement une fois, ce qui transforme O(2^n) en O(n) en temps et en O(n) d’espace pour le dictionnaire de mémo, auquel s’ajoutent O(n) d’espace de pile.
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!Décorateur functools.lru_cache
Python fournit @functools.lru_cache(maxsize=None) (également disponible sous la forme @functools.cache depuis Python 3.9) pour automatiser la mémoïsation. L’ajout de ce décorateur au-dessus d’une fonction met en cache tous ses appels en fonction de leurs arguments. maxsize=None signifie que la taille du cache est illimitée : chaque combinaison d’arguments distincte est mise en cache. Cela transforme n’importe quelle fonction récursive en version mémoïsée avec une seule ligne de code.
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=...)Escaliers (LeetCode 70)
Le problème 70 de LeetCode, « Escaliers » : vous pouvez monter 1 ou 2 marches à la fois. Combien de façons existe-t-il d’atteindre la marche n ? Il s’agit en réalité de Fibonacci : ways(n) = ways(n-1) + ways(n-2). Cas de base : ways(0) = 1 (une façon de rester au sol) et ways(1) = 1. Avec la mémoïsation, le temps d’exécution est O(n) et l’espace utilisé est 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,21Rendu de monnaie (LeetCode 322)
Le problème 322 de LeetCode, « Rendu de monnaie » : étant donné des valeurs de pièces et un montant cible, trouvez le nombre minimal de pièces. Récursion mémoïsée descendante : dp(amount) = 1 + min(dp(amount - coin)) pour chaque pièce valide. Cas de base : dp(0) = 0. Mettez en cache chaque sous-montant. Si un sous-montant est impossible à obtenir, renvoyez l’infini. La mémoïsation transforme la force brute exponentielle en une complexité temporelle de O(montant × longueur(pièces)).
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)) # -1Découpage de mots (LeetCode 139) avec mémoïsation
Le problème 139 de LeetCode, « Découpage de mots » : déterminez si une chaîne peut être segmentée en mots du dictionnaire. La récursion descendante can_break(s, start) essaie chaque préfixe s[start:end] ; s’il se trouve dans le dictionnaire et que can_break(s, end) est vrai, renvoyez vrai. Sans mémoïsation, la complexité est O(2^n) ; avec mémoïsation (en mettant en cache chaque indice de début), elle devient O(n² × L), où L est la longueur maximale d’un mot.
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'])) # FalseMémoïsation ou tabulation
La mémoïsation (descendante) part du problème initial et met en cache les réponses au fur et à mesure qu’elles sont découvertes récursivement. Elle ne résout que les sous-problèmes réellement nécessaires. La tabulation (ascendante) remplit au préalable un tableau, des petits sous-problèmes vers les grands, et résout tous les sous-problèmes sans exception. La mémoïsation est plus facile à déduire d’une solution récursive ; la tabulation évite les limites de profondeur de récursion et la surcharge des appels de fonction.
# 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 limitOptimisation de l’espace : variables glissantes
De nombreux problèmes de DP que la récursion mémoïsée résout en O(n) d’espace peuvent être optimisés davantage jusqu’à O(1) d’espace lorsque seul un nombre fixe de réponses précédentes de sous-problèmes est nécessaire. Pour Fibonacci, seules les deux dernières valeurs comptent. Il en va de même pour les escaliers. Deux variables glissantes remplacent l’intégralité du dictionnaire de mémo ou du tableau.
# 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 ou fermeture ou dictionnaire global
Il existe trois façons d’implémenter manuellement la mémoïsation. Un dictionnaire global est simple, mais il pollue la portée du module. Une fermeture encapsule le cache dans la fonction, ce qui empêche les fuites, mais nécessite une fonction enveloppe. @lru_cache est la solution la plus élégante : un seul décorateur remplace tout le code répétitif. Lors d’un entretien, commencez par @lru_cache, sauf si l’interviewer demande explicitement une implémentation manuelle.
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 832040Quand la mémoïsation n’est d’aucune aide
La mémoïsation n’accélère que les problèmes présentant des sous-problèmes qui se chevauchent — c’est-à-dire les cas où le même sous-problème est calculé plusieurs fois. Si chaque sous-problème est unique (comme lors d’un simple parcours d’arbre où chaque nœud est visité exactement une fois), la mémoïsation ajoute une surcharge sans apporter de bénéfice. Elle ne peut pas non plus résoudre les problèmes où l’arbre récursif est exponentiel par rapport au nombre de sous-problèmes distincts plutôt qu’à leur réutilisation ; ceux-ci nécessitent un algorithme entièrement différent.
# 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')Récapitulatif : liste de contrôle de la mémoïsation
Appliquez la mémoïsation lorsque : vous disposez d’une solution récursive correcte mais lente en raison de recalculs redondants, la fonction comporte un petit nombre de combinaisons d’arguments distinctes et la valeur renvoyée dépend uniquement des arguments (fonction pure — sans effets secondaires ni état global). Examinez l’espace d’états des sous-problèmes : s’il y a au plus O(n) ou O(n²) états distincts, la mémoïsation transforme une complexité exponentielle en complexité polynomiale.
Vérification rapide
Évaluez votre compréhension des concepts de structures de données et d’algorithmes — préparation aux entretiens de programmation présentés dans cette leçon.
Récapitulatif de la leçon
Dans cette leçon, vous avez appris que : la mémoïsation stocke les résultats des sous-problèmes pour éviter les recalculs, transformant une récursion exponentielle en complexité polynomiale ; @functools.lru_cache est l’outil Python idiomatique qui ne nécessite qu’une seule ligne ; et la mémoïsation (descendante) et la tabulation (ascendante) sont les deux styles de DP : la mémoïsation est plus facile à déduire, tandis que la tabulation évite les problèmes de profondeur de pile. Félicitations : vous avez terminé les modules sur la récursion et les tables de hachage !
Questions Fréquemment Posées
La leçon « Mémoïsation : mettre en cache les résultats récursifs » est-elle gratuite ?
Oui — le texte complet de « Mémoïsation : mettre en cache les résultats récursifs » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours Coding Interview Prep, passe à CoddyKit PRO. Le cours Coding Interview Prep comprend 4 leçons au total.
Qu'est-ce que j'apprendrai dans « Mémoïsation : mettre en cache les résultats récursifs » ?
Appliquez @functools.lru_cache et des dictionnaires de mémoïsation manuelle à Fibonacci et climbing-stairs pour éliminer les recalculs exponentiels. Tu pratiques Coding Interview Prep avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.
Dois-je avoir de l'expérience pour commencer Coding Interview Prep ?
Aucune expérience préalable n'est requise. Coding Interview Prep sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 4 sur 4.
Combien de temps prend la leçon « Mémoïsation : mettre en cache les résultats récursifs » ?
La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.
Peux-tu écrire et exécuter du code dans cette leçon Coding Interview Prep ?
Oui. Chaque leçon Coding Interview Prep inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.
Toutes les leçons de ce cours
- Cadre de la récursivité : cas de base, confiance, construction
- Visualiser la pile d’appels
- Compromis entre récursivité et itération
- Mémoïsation : mettre en cache les résultats récursifs