Mémoriser pour ne pas recalculer

Fibonacci naïf et l'explosion de l'arbre d'appels

La suite de Fibonacci se définit simplement : fib(0)=0, fib(1)=1, et pour n≥2, fib(n)=fib(n-1)+fib(n-2). La traduction directe en fonction récursive semble naturelle :

def fib_naif(n):
    if n <= 1:
        return n
    return fib_naif(n - 1) + fib_naif(n - 2)

Ce code est correct, mais catastrophiquement lent. Pourquoi ? Parce que pour calculer fib(n), la fonction appelle fib(n-1) ET fib(n-2), qui eux-mêmes rappellent des sous-parties déjà calculées ailleurs dans l'arbre. Regarde l'arbre d'appels complet de fib(5) :

fib(5)
  fib(4)
    fib(3)
      fib(2)
        fib(1)
        fib(0)
      fib(1)
    fib(2)
      fib(1)
      fib(0)
  fib(3)                (recalcule tout ce sous-arbre, deja fait plus haut !)
    fib(2)
      fib(1)
      fib(0)
    fib(1)

Pour ce petit exemple avec n=5, cela représente déjà 15 appels de fonction, alors qu'il n'existe que 6 valeurs distinctes (fib(0) à fib(5)) à calculer. Le nœud fib(3) est recalculé entièrement deux fois, fib(2) trois fois, fib(1) cinq fois : à chaque niveau, le nombre d'appels redondants augmente.

En réalité, le nombre total d'appels de fib_naif(n) croît de façon exponentielle avec n (proche de 1.618^n, le nombre d'or). Pour n=40, cela représente des centaines de millions d'appels : le programme devient inutilisable en pratique, alors que le calcul « à la main » ne nécessite que 40 additions si on s'y prend intelligemment.

Le problème n'est donc pas la formule mathématique, qui est juste, mais la façon dont on la calcule : on refait sans cesse le même travail parce qu'on ne garde aucune trace des résultats déjà obtenus.