J'ai pris des mathématiques discrètes (dans lesquelles j'ai appris sur le théorème principal, Big Theta/Omega/O) il y a quelque temps et il me semble avoir oublié la différence entre O(logn) et O (2 ^ n) ( pas dans le sens théorique de Big Oh). Je comprends généralement que les algorithmes tels que la fusion et le tri rapide sont O(nlogn) parce qu'ils divisent à plusieurs reprises le tableau d'entrée initial en sous-tableaux jusqu'à ce que chaque sous-tableau ait la taille 1 avant de revenir en arrière dans l'arborescence, ce qui donne une récursivité. arbre qui est de hauteur logn + 1. Mais si vous calculez la hauteur d'un arbre récursif en utilisant n/b ^ x = 1 (lorsque la taille du sous-problème est devenue 1 comme indiqué dans une réponse ici ), il semble que vous obtenez toujours que la hauteur de l’arbre est log (n).
Si vous résolvez la séquence de Fibonacci en utilisant la récursivité, je pense que vous obtiendrez également un arbre de taille logn, mais pour une raison quelconque, le Big O de l'algorithme est O (2 ^ n). Je pensais que la différence était peut-être due au fait que vous deviez vous souvenir de tous les nombres de fib de chaque sous-problème pour obtenir le nombre de fib réel, ce qui signifie que la valeur de chaque nœud doit être rappelée, mais il semble que, dans le tri par fusion, la valeur de chaque nœud doit également être utilisé (ou au moins trié). Cela ne ressemble toutefois pas à la recherche binaire, où vous ne visitez que certains nœuds en vous basant sur des comparaisons effectuées à chaque niveau de l’arbre, je pense donc que c’est de là que vient la confusion.
Donc précisément, qu'est-ce qui fait que la séquence de Fibonacci a une complexité temporelle différente de celle d'algorithmes comme le tri par fusion/fusion?
Les autres réponses sont correctes, mais n'indiquez pas clairement. D'où vient la grande différence entre l'algorithme de Fibonacci et les algorithmes diviser pour régner? En effet, la forme de l’arbre de récurrence pour les deux classes de fonctions est la même: c’est un arbre binaire.
Le truc à comprendre est en réalité très simple: considérons le size de l’arbre de récursivité en fonction de la taille d’entrée n
.
Rappelons quelques faits de base sur les arbres binaires first:
n
est un arbre binaire est égal au nombre de nœuds non-feuilles plus un. La taille d'un arbre binaire est donc 2n-1.h
pour un arbre binaire parfait à n
feuilles est égale à log(n)
, pour un arbre binaire aléatoire: h = O(log(n))
et pour un arbre binaire dégénéré h = n-1
.Intuitivement:
Pour trier un tableau d'éléments n
avec un algorithme récursif, l'arbre de récursivité a n
leaves . Il s’ensuit que la largeur de l’arbre estn
, la hauteur de l’arbre estO(log(n))
en moyenne etO(n)
dans le pire des cas.
Pour calculer un élément de séquence Fibonacci k
avec l'algorithme récursif, l'arbre de récursion a k
levels (pour savoir pourquoi, considérons que fib(k)
appelle fib(k-1)
, qui appelle fib(k-2)
, etc.). Il en résulte que hauteur de l’arbre estk
. Pour estimer une limite inférieure de la largeur et du nombre de nœuds dans l’arbre de récursivité, supposons que, puisque fib(k)
appelle également fib(k-2)
, il existe donc un parfait arbre binaire de hauteur k/2
faisant partie de l’arbre de récursivité. Si extrait, ce sous-arbre parfait aurait 2k/2 nœuds feuilles. Donc, la largeur de l'arbre de récurrence est au moins O(2^{k/2})
ou, de manière équivalente,2^O(k)
.
La différence cruciale est que:
Par conséquent, le nombre de nœuds dans l’arborescence est O(n)
dans le premier cas, mais 2^O(n)
dans le second. L'arbre de Fibonacci est beaucoup plus grand par rapport à la taille d'entrée.
Vous mentionnez théorème principal ; Cependant, le théorème ne peut pas être appliqué à l'analyse de la complexité de Fibonacci car il ne s'applique qu'aux algorithmes où l'entrée est en fait divisée à chaque niveau de récursivité. Fibonacci ne pas divise l'entrée; En fait, les fonctions de niveau i
produisent presque deux fois plus d'entrées pour le niveau suivant i+1
.
Pour répondre au cœur de la question, c’est "pourquoi Fibonacci et non Mergesort", vous devez vous concentrer sur cette différence cruciale:
Pour voir ce que je veux dire par "calcul répété", regardez l'arbre pour le calcul de F (6):
Photo de l'arbre de Fibonacci de: http://composingprograms.com/pages/28-efficiency.html
Combien de fois voyez-vous que F(3) a été calculé?
Considérez l'implémentation suivante
int fib(int n)
{
if(n < 2)
return n;
return fib(n-1) + fib(n-2)
}
Notons T(n) le nombre d'opérations que fib
effectue pour calculer fib(n)
. Étant donné que fib(n)
appelle fib(n-1)
et fib(n-2)
, cela signifie que T(n) est au moins T(n-1) + T(n-2)
. Cela signifie que T(n) > fib(n)
. Il existe une formule directe de fib(n)
qui est une constante du pouvoir de n
. Par conséquent, T(n) est au moins exponentiel. QED.
Avec l’algo récursif, vous avez environ 2 ^ N opérations (additions) pour fibonacci (N). Ensuite c'est O (2 ^ N).
Avec un cache (mémorisation), vous avez environ N opérations, puis c'est O (N).
Les algorithmes avec complexité O (N log N) sont souvent une conjonction d'itération sur chaque élément (O (N)), de fractionnement de recurse et de fusion ... Split by 2 => vous enregistrez des récursions de N.
Si j'ai bien compris, l'erreur dans votre raisonnement est que l'utilisation d'une implémentation récursive pour évaluer f(n)
où f
désigne la séquence de Fibonacci, la taille d'entrée est réduite d'un facteur 2 (ou d'un autre facteur), ce qui n'est pas le cas. Chaque appel (à l'exception des «cas de base» 0 et 1) utilise exactement 2 appels récursifs, car il n'est pas possible de réutiliser des valeurs calculées précédemment. À la lumière de la présentation du théorème maître sur Wikipedia , la récurrence
f(n) = f (n-1) + f(n-2)
est un cas pour lequel le théorème maître ne peut pas être appliqué.
La complexité du temps de tri de la fusion est O (n log (n)). Le meilleur cas de tri rapide est O (n log (n)), le pire des cas O (n ^ 2).
Les autres réponses expliquent pourquoi Fibonacci naïf récursif est O (2 ^ n).
Si vous lisez que Fibonacci (n) peut être O (log (n)), cela est possible si vous le calculez par itération et quadrature répétée en utilisant la méthode de la matrice ou la méthode de la séquence de lucas. Exemple de code pour la méthode de séquence lucas (notez que n est divisé par 2 sur chaque boucle):
/* lucas sequence method */
int fib(int n) {
int a, b, p, q, qq, aq;
a = q = 1;
b = p = 0;
while(1) {
if(n & 1) {
aq = a*q;
a = b*q + aq + a*p;
b = b*p + aq;
}
n /= 2;
if(n == 0)
break;
qq = q*q;
q = 2*p*q + qq;
p = p*p + qq;
}
return b;
}