web-dev-qa-db-fra.com

Complexité informatique de la séquence de Fibonacci

Je comprends la notation Big-O, mais je ne sais pas la calculer pour beaucoup de fonctions. En particulier, j'ai essayé de comprendre la complexité informatique de la version naïve de la séquence de Fibonacci:

int Fibonacci(int n)
{
    if (n <= 1)
        return n;
    else
        return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Quelle est la complexité de calcul de la séquence de Fibonacci et comment est-elle calculée?

282
Juliet

Vous modélisez la fonction time pour calculer Fib(n) sous la forme d'une somme de temps pour calculer Fib(n-1), plus le temps nécessaire pour calculer Fib(n-2), ainsi que le temps nécessaire pour les additionner (O(1)). Cela suppose que les évaluations répétées de la même Fib(n) prennent le même temps - c’est-à-dire qu’aucune mémorisation n’est utilisée.

T(n<=1) = O(1)

T(n) = T(n-1) + T(n-2) + O(1)

Vous résolvez cette relation de récurrence (à l'aide de fonctions génératrices, par exemple) et vous obtenez la réponse.

Alternativement, vous pouvez dessiner l’arbre de récursion, qui aura la profondeur n et comprendre intuitivement que cette fonction est asymptotiquement O(2n). Vous pouvez ensuite prouver votre conjecture par induction.

Base: n = 1 est évident

Supposer que T(n-1) = O(2n-1)donc

T(n) = T(n-1) + T(n-2) + O(1) qui est égal à

T(n) = O(2n-1) + O(2n-2) + O(1) = O(2n)

Cependant, comme indiqué dans un commentaire, ce n'est pas la limite. Un fait intéressant à propos de cette fonction est que le T(n) est asymptotiquement le même comme valeur de Fib(n) puisque les deux sont définis comme

f(n) = f(n-1) + f(n-2).

Les feuilles de l'arbre de récurrence renverront toujours 1. La valeur de Fib(n) est la somme de toutes les valeurs renvoyées par les feuilles dans l'arbre de récurrence, ce qui correspond au nombre de feuilles. Puisque chaque feuille prendra O(1) à calculer, T(n) est égal à Fib(n) x O(1). Par conséquent, la limite étroite de cette fonction est la séquence de Fibonacci elle-même (~ θ(1.6n)). Vous pouvez découvrir ce lien étroit en utilisant les fonctions de génération comme je l'avais mentionné plus haut.

343
Mehrdad Afshari

Demandez-vous simplement combien d'instructions doivent être exécutées pour que F(n) soit terminé.

Pour F(1), la réponse est 1 (la première partie du conditionnel).

Pour F(n), la réponse est F(n-1) + F(n-2).

Alors, quelle fonction satisfait ces règles? Essayer unn (a> 1):

unen == a(n-1) + a(n-2)

Diviser par un(n-2):

une2 == a + 1

Résolvez pour a et vous obtenez (1+sqrt(5))/2 = 1.6180339887, également connu sous le nom de nombre d'or .

Donc, cela prend du temps exponentiel.

114
Jason Cohen

Il y a une très bonne discussion de ce problème spécifique sur MIT . À la page 5, ils soulignent que, si vous supposez qu'une addition prend une unité de calcul, le temps requis pour calculer Fib (N) est très étroitement lié au résultat de Fib (N).

En conséquence, vous pouvez passer directement à l’approximation très proche de la série de Fibonacci:

Fib(N) = (1/sqrt(5)) * 1.618^(N+1) (approximately)

et dire, par conséquent, que la pire performance de l'algorithme naïf est 

O((1/sqrt(5)) * 1.618^(N+1)) = O(1.618^(N+1))

PS: Il y a une discussion sur le expression sous forme fermée du numéro de Nth Fibonacci } sur Wikipedia si vous souhaitez plus d'informations.

28
Bob Cross

Je suis d'accord avec pgaur et rickerbh, la complexité de récursive-fibonacci est O (2 ^ n).

Je suis arrivé à la même conclusion par un raisonnement plutôt simpliste mais qui reste valable.

Tout d’abord, il s’agit de déterminer combien de fois la fonction fibonacci récursive (F() à partir de maintenant) est appelée lors du calcul du Nième nombre de fibonacci. Si on l’appelle une fois par numéro dans l’ordre 0 à n, on a O (n), s’il est appelé n fois pour chaque numéro, on obtient O (n * n) ou O (n ^ 2), etc.

Ainsi, lorsque F() est appelé pour un nombre n, le nombre de fois F() est appelé pour un nombre donné compris entre 0 et n-1 augmente à mesure que nous approchons de 0.

Comme première impression, il me semble que si nous le posons de manière visuelle, dessiner une unité par heure F() est appelé pour un nombre donné, obtenant ainsi une sorte de pyramide (c'est-à-dire , si nous centrons les unités horizontalement). Quelque chose comme ça:

n              *
n-1            **
n-2           ****  
...
2           ***********
1       ******************
0    ***************************

Maintenant, la question est: à quelle vitesse la base de cette pyramide s’agrandit-elle à mesure que n grandit?

Prenons un cas réel, par exemple F (6)

F(6)                 *  <-- only once
F(5)                 *  <-- only once too
F(4)                 ** 
F(3)                ****
F(2)              ********
F(1)          ****************           <-- 16
F(0)  ********************************    <-- 32

Nous voyons F(0) est appelé 32 fois, ce qui correspond à 2 ^ 5, soit 2 ^ (n-1) pour cet exemple.

Maintenant, nous voulons savoir combien de fois F(x) est appelé du tout, et nous pouvons voir le nombre de fois F(0) est appelé n'est qu'une partie de ce . 

Si nous déplaçons mentalement tous les * de F(6) à F(2) lignes dans F(1) ligne, nous voyons que F(1) et F(0) les lignes ont maintenant la même longueur. Ce qui signifie que le total des temps F() est appelé lorsque n = 6 est 2x32 = 64 = 2 ^ 6.

Maintenant, en termes de complexité:

O( F(6) ) = O(2^6)
O( F(n) ) = O(2^n)
26
J.P.

Vous pouvez l'agrandir et avoir une visualisation 

     T(n) = T(n-1) + T(n-2) <
     T(n-1) + T(n-1) 

     = 2*T(n-1)   
     = 2*2*T(n-2)
     = 2*2*2*T(n-3)
     ....
     = 2^i*T(n-i)
     ...
     ==> O(2^n)
15
Tony Tannous

Il est limité au bas par 2^(n/2) et au haut par 2 ^ n (comme indiqué dans d'autres commentaires). Et un fait intéressant de cette implémentation récursive est qu’elle possède une liaison asymptotique étroite de Fib (n) elle-même. Ces faits peuvent être résumés:

T(n) = Ω(2^(n/2))  (lower bound)
T(n) = O(2^n)   (upper bound)
T(n) = Θ(Fib(n)) (tight bound)

Le lien étroit peut être réduit davantage en utilisant sa forme fermée si vous voulez.

9
Dave L.

Les réponses de preuve sont bonnes, mais je dois toujours faire quelques itérations à la main pour vraiment me convaincre. J'ai donc dessiné un petit arbre d'appel sur mon tableau blanc et commencé à compter les nœuds. Je divise mes comptes en nœuds totaux, en nœuds feuilles et en nœuds intérieurs. Voici ce que j'ai eu:

IN | OUT | TOT | LEAF | INT
 1 |   1 |   1 |   1  |   0
 2 |   1 |   1 |   1  |   0
 3 |   2 |   3 |   2  |   1
 4 |   3 |   5 |   3  |   2
 5 |   5 |   9 |   5  |   4
 6 |   8 |  15 |   8  |   7
 7 |  13 |  25 |  13  |  12
 8 |  21 |  41 |  21  |  20
 9 |  34 |  67 |  34  |  33
10 |  55 | 109 |  55  |  54

Ce qui saute immédiatement aux yeux, c'est que le nombre de nœuds feuilles est fib(n). Il a fallu quelques itérations de plus pour constater que le nombre de nœuds intérieurs est fib(n) - 1. Par conséquent, le nombre total de nœuds est 2 * fib(n) - 1.

Puisque vous supprimez les coefficients lors de la classification de la complexité informatique, la réponse finale est θ(fib(n)).

9
benkc

La complexité temporelle de l'algorithme récursif peut être mieux estimée en traçant un arbre de récurrence. Dans ce cas, la relation de récurrence pour dessiner un arbre de récurrence serait T (n) = T (n-1) + T (n-2) + O (1) note que note chaque étape prend O(1) signifiant le temps constant, car il ne fait qu'une comparaison pour vérifier la valeur de n dans si block.Recursion tree ressemblerait à

          n
   (n-1)      (n-2)
(n-2)(n-3) (n-3)(n-4) ...so on

Ici, disons que chaque niveau de l’arbre ci-dessus est désigné par i

i
0                        n
1            (n-1)                 (n-2)
2        (n-2)    (n-3)      (n-3)     (n-4)
3   (n-3)(n-4) (n-4)(n-5) (n-4)(n-5) (n-5)(n-6)

disons qu'à une valeur particulière de i, l'arbre se termine, ce cas serait le cas où ni = 1, d'où i = n-1, ce qui signifie que la hauteur de l'arbre est n-1 . Voyons maintenant combien de travail est nécessaire. fait pour chacune des n couches de l'arbre.Notez que chaque étape prend O(1) le temps indiqué dans la relation de récurrence.

2^0=1                        n
2^1=2            (n-1)                 (n-2)
2^2=4        (n-2)    (n-3)      (n-3)     (n-4)
2^3=8   (n-3)(n-4) (n-4)(n-5) (n-4)(n-5) (n-5)(n-6)    ..so on
2^i for ith level

puisque i = n-1 est la hauteur de l'arbre, le travail effectué à chaque niveau sera

i work
1 2^1
2 2^2
3 2^3..so on

Par conséquent, le travail total effectué correspond à la somme des travaux effectués à chaque niveau. Il sera donc égal à 2 ^ 0 + 2 ^ 1 + 2 ^ 2 + 2 ^ 3 ... + 2 ^ (n-1) puisque i = n-1. En séries géométriques, cette somme est 2 ^ n, d’où la complexité temporelle totale ici est O (2 ^ n)

5
nikhil kekan

Eh bien, selon moi, c’est O(2^n) car dans cette fonction, seule la récursivité prend le temps considérable (diviser pour régner). Nous voyons que la fonction ci-dessus continuera dans un arbre jusqu'à ce que les feuilles soient proches lorsque nous atteindrons le niveau F(n-(n-1)), c'est-à-dire F(1). Donc, ici, quand nous notons la complexité temporelle rencontrée à chaque profondeur d'arbre, la série de sommation est:

1+2+4+.......(n-1)
= 1((2^n)-1)/(2-1)
=2^n -1

c'est l'ordre de 2^n [ O(2^n) ].

2
pgaur
1
nsh3

La version naïve de récursion de Fibonacci est de conception exponentielle en raison de la répétition dans le calcul:

À la racine, vous calculez:

F(n) depends on F(n-1) and F(n-2)

F(n-1) depends on F(n-2) again and F(n-3)

F(n-2) depends on F(n-3) again and F(n-4)

alors vous avez à chaque niveau 2 des appels récursifs qui gaspillent beaucoup de données dans le calcul, la fonction de temps ressemblera à ceci:

T (n) = T(n-1) + T(n-2) + C, avec C constant

T (n-1) = T(n-2) + T(n-3)> T(n-2) puis

T(n) > 2*T(n-2)

...

T(n) > 2^(n/2) * T(1) = O(2^(n/2))

Ceci est juste une limite inférieure qui, aux fins de votre analyse, devrait suffire, mais la fonction temps réel est un facteur de constante de la même formule de Fibonacci et la forme fermée est connue pour être exponentielle du ratio d'or.

De plus, vous pouvez trouver des versions optimisées de Fibonacci en utilisant une programmation dynamique comme celle-ci:

static int fib(int n)
{
    /* memory */
    int f[] = new int[n+1];
    int i;

    /* Init */
    f[0] = 0;
    f[1] = 1;

    /* Fill */
    for (i = 2; i <= n; i++)
    {
        f[i] = f[i-1] + f[i-2];
    }

    return f[n];
}

Ceci est optimisé et ne fait que n étapes mais est également exponentiel.

Les fonctions de coût sont définies de la taille d'entrée au nombre d'étapes permettant de résoudre le problème. Lorsque vous voyez la version dynamique de Fibonacci (n étapes pour calculer la table) ou l’algorithme le plus simple pour savoir si un nombre est premier (sqrt (n) pour analyser les diviseurs valides du nombre). vous pensez peut-être que ces algorithmes sont O(n) ou O(sqrt(n)), mais ce n'est tout simplement pas vrai pour la raison suivante: l’entrée dans votre algorithme est un nombre: n, en utilisant la notation binaire, la taille d’entrée pour un entier n est log2 (n), puis on modifie

m = log2(n) // your real input size

permet de connaître le nombre d'étapes en fonction de la taille d'entrée

m = log2(n)
2^m = 2^log2(n) = n

alors le coût de votre algorithme en fonction de la taille d'entrée est:

T(m) = n steps = 2^m steps

et c’est pourquoi le coût est exponentiel.

0
Miguel