Big O, comment calculez-vous/approximatif?
la plupart des diplômés en sciences savent certainement ce que Big O signifie . Il nous aide à mesurer comment (dans)efficace un algorithme est vraiment et si vous savez dans quelle catégorie le problème que vous essayez de résoudre se trouve dans vous pouvez comprendre s'il est encore possible d'extraire cette petite performance supplémentaire. 1
mais je suis curieux, comment vous calculent ou la complexité de vos algorithmes?
1 mais comme ils disent, ne l'exagérez pas, l'optimisation prématurée est la racine de tout mal , et l'optimisation sans cause justifiée devrait mériter ce nom aussi bien.
22 réponses
je suis un professeur assistant à mon université locale sur les Structures de données et les algorithmes cours. Je ferai de mon mieux pour l'expliquer ici en termes simples, mais sachez que ce sujet prend quelques mois à mes élèves pour le saisir. Vous pouvez trouver plus d'informations sur le Chapitre 2 du Structures de données et algorithmes en Java Livre.
il n'y a pas procédure mécanique que peut être utilisé pour obtenir le BigOh.
comme un" livre de cuisine", pour obtenir le BigOh à partir d'un morceau de code, vous devez d'abord vous rendre compte que vous créez une formule mathématique pour compter combien d'étapes de calculs sont exécutés à partir d'une entrée d'une certaine taille.
le but est simple: comparer les algorithmes d'un point de vue théorique, sans devoir exécuter le code. Plus le nombre d'étapes est petit, plus l'algorithme est rapide.
Par exemple, disons que vous avez ce morceau de code:
int sum(int* data, int N) {
int result = 0; // 1
for (int i = 0; i < N; i++) { // 2
result += data[i]; // 3
}
return result; // 4
}
cette fonction renvoie la somme de tous les éléments du tableau, et nous voulons créer une formule pour compter la complexité computationnelle de cette fonction:
Number_Of_Steps = f(N)
nous avons Donc f(N)
, une fonction pour compter le nombre d'étapes de calcul. L'entrée de la fonction est la taille de la structure à traiter. Cela signifie que cette fonction est appelée comme:
Number_Of_Steps = f(data.length)
le paramètre N
prend la valeur data.length
. Maintenant, nous avons besoin de la définition de la fonction f()
. Cela se fait à partir du code source, dans lequel chaque ligne intéressante est numérotée de 1 à 4.
il y a plusieurs façons de calculer le BigOh. À partir de ce point, nous allons supposer que chaque phrase qui ne dépend pas de la taille de l'entrée de données prend une constante C
nombre étapes de calcul.
nous allons ajouter le nombre individuel d'étapes de la fonction, et ni la déclaration de variable locale ni la déclaration de retour dépend de la taille du tableau data
.
cela signifie que les lignes 1 et 4 prennent C quantité de pas chacun, et la fonction est un peu comme ceci:
f(N) = C + ??? + C
La prochaine partie consiste à définir la valeur de la for
déclaration. Rappelez-vous que nous comptons le nombre d'étapes de calcul, ce qui signifie que le corps de la for
déclaration est exécuté N
fois. C'est la même chose que d'ajouter C
, N
fois:
f(N) = C + (C + C + ... + C) + C = C + N * C + C
il n'y a pas de règle mécanique pour compter combien de fois le corps du for
est exécuté, vous devez le Compter en regardant ce que fait le code. Pour simplifier les calculs, nous ignorons la parties variables d'initialisation, de condition et d'incrément de la déclaration for
.
pour obtenir le BigOh réel nous avons besoin de la analyse asymptotique de la fonction. C'est grossièrement fait comme ceci:
- enlevez toutes les constantes
C
. - à Partir de
f()
get polynomium dans sonstandard form
. - diviser les termes du polynôme et les Trier par le taux de croissance.
- Garder celui qui prend de l'ampleur lorsque
N
approchesinfinity
.
notre f()
a deux termes:
f(N) = 2 * C * N ^ 0 + 1 * C * N ^ 1
enlevant toutes les constantes C
et les parties redondantes:
f(N) = 1 + N ^ 1
puisque le dernier terme est celui qui grandit quand f()
approche l'infini (pensez à limits ) c'est L'argument BigOh, et la fonction sum()
a un BigOh de:
O(N)
il y a quelques astuces pour résoudre certaines difficultés: utilisez sommations chaque fois que vous le pouvez.
à titre d'exemple, ce code peut être facilement résolu en utilisant des sommations:
for (i = 0; i < 2*n; i += 2) { // 1
for (j=n; j > i; j--) { // 2
foo(); // 3
}
}
la première chose à vous demander est l'ordre de l'exécution de foo()
. Alors que l'habitude est d'être O(1)
, vous devez demander à vos professeurs à ce sujet. O(1)
signifie (presque, principalement) constante C
, indépendante de la taille N
.
l'énoncé for
sur la phrase numéro un est délicat. Alors que l'indice se termine à 2 * N
, l'incrémentation se fait par deux. Cela signifie que le premier for
est exécuté seulement N
pas, et nous avons besoin de diviser le compte par de deux.
f(N) = Summation(i from 1 to 2 * N / 2)( ... ) =
= Summation(i from 1 to N)( ... )
le numéro de phrase deux est encore plus délicat puisqu'il dépend de la valeur de i
. Prendre un coup d'oeil: l'indice i prend les valeurs: 0, 2, 4, 6, 8, ..., 2 * N, et le second for
exécuté: N fois la première, N - 2, le deuxième, N - 4 le troisième... jusqu'à la scène N / 2, sur laquelle le second for
ne sera jamais exécuté.
sur la formule, qui signifie:
f(N) = Summation(i from 1 to N)( Summation(j = ???)( ) )
encore une fois, nous comptons le nombre de pas . Et par définition, chaque sommation devrait toujours commencer par un, et se terminer par un nombre plus grand-ou-égal qu'un.
f(N) = Summation(i from 1 to N)( Summation(j = 1 to (N - (i - 1) * 2)( C ) )
(nous supposons que foo()
est O(1)
et prend C
pas.)
nous avons ici un problème: lorsque i
prend la valeur N / 2 + 1
vers le haut, la sommation interne se termine par un négatif nombre! C'est impossible et le mal. Nous devons diviser la somme en deux, étant le point central le moment i
prend N / 2 + 1
.
f(N) = Summation(i from 1 to N / 2)( Summation(j = 1 to (N - (i - 1) * 2)) * ( C ) ) + Summation(i from 1 to N / 2) * ( C )
depuis le moment charnière i > N / 2
, l'intérieur for
ne sera pas exécuté, et nous présumons une constante c exécution complexité sur son corps.
maintenant les sommations peuvent être simplifiées en utilisant certaines règles d'identité:
- sommation(w de 1 À N) (C) = N * C
- sommation (w de 1 À N) (A ( + / - ) B ) = sommation( w de 1 À N) (a) ( + / - ) sommation( w de 1 à N) (B )
- Sommation(w de 1 à N)( w * C ) = C * Sommation(w de 1 à N)( w ) (C est une constante, indépendante de
w
) - sommation (w de 1 à N) (w) = (N * (N + 1)) / 2
application d'une algèbre:
f(N) = Summation(i from 1 to N / 2)( (N - (i - 1) * 2) * ( C ) ) + (N / 2)( C )
f(N) = C * Summation(i from 1 to N / 2)( (N - (i - 1) * 2)) + (N / 2)( C )
f(N) = C * (Summation(i from 1 to N / 2)( N ) - Summation(i from 1 to N / 2)( (i - 1) * 2)) + (N / 2)( C )
f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2)( i - 1 )) + (N / 2)( C )
=> Summation(i from 1 to N / 2)( i - 1 ) = Summation(i from 1 to N / 2 - 1)( i )
f(N) = C * (( N ^ 2 / 2 ) - 2 * Summation(i from 1 to N / 2 - 1)( i )) + (N / 2)( C )
f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N / 2 - 1) * (N / 2 - 1 + 1) / 2) ) + (N / 2)( C )
=> (N / 2 - 1) * (N / 2 - 1 + 1) / 2 =
(N / 2 - 1) * (N / 2) / 2 =
((N ^ 2 / 4) - (N / 2)) / 2 =
(N ^ 2 / 8) - (N / 4)
f(N) = C * (( N ^ 2 / 2 ) - 2 * ( (N ^ 2 / 8) - (N / 4) )) + (N / 2)( C )
f(N) = C * (( N ^ 2 / 2 ) - ( (N ^ 2 / 4) - (N / 2) )) + (N / 2)( C )
f(N) = C * (( N ^ 2 / 2 ) - (N ^ 2 / 4) + (N / 2)) + (N / 2)( C )
f(N) = C * ( N ^ 2 / 4 ) + C * (N / 2) + C * (N / 2)
f(N) = C * ( N ^ 2 / 4 ) + 2 * C * (N / 2)
f(N) = C * ( N ^ 2 / 4 ) + C * N
f(N) = C * 1/4 * N ^ 2 + C * N
et le BigOh est:
O(N²)
Big O donne la limite supérieure de la complexité temporelle d'un algorithme. Il est généralement utilisé en conjonction avec le traitement des séries de données (listes), mais peut être utilisé ailleurs.
quelques exemples de la façon dont il est utilisé dans le code C.
Dire que nous avons un tableau de n éléments
int array[n];
si nous voulions accéder au premier élément du tableau ce serait O (1) puisqu'il n'importe pas combien le tableau est grand, il prend toujours le même la constante de temps pour obtenir le premier élément.
x = array[0];
Si nous voulions trouver un numéro dans la liste:
for(int i = 0; i < n; i++){
if(array[i] == numToFind){ return i; }
}
ce serait O(n) puisqu'au plus nous aurions à regarder à travers la liste entière pour trouver notre numéro. Le Big-O est toujours O (n) même si nous pourrions trouver notre numéro le premier essai et courir à travers la boucle une fois parce que le Big-O décrit la limite supérieure pour un algorithme (omega est pour la limite inférieure et theta est pour la limite serrée).
quand on arrive aux boucles imbriquées:
for(int i = 0; i < n; i++){
for(int j = i; j < n; j++){
array[j] += 2;
}
}
C'est O(N^2) puisque pour chaque passage de la boucle externe ( O(n) ) nous devons parcourir à nouveau toute la liste pour que les n se multiplient nous laissant avec n au carré.
c'est à peine gratter la surface mais quand vous arrivez à analyser des algorithmes plus complexes mathématiques complexes impliquant des preuves entre en jeu. Espérons que cela vous familiarise avec les bases au moins.
bien qu'il soit utile de savoir comment trouver le meilleur moment pour votre problème particulier, connaître certains cas généraux peut vous aider à prendre des décisions dans votre algorithme.
voici quelques-uns des cas les plus courants, enlevés de http://en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions :
O (1) - Déterminer si un nombre est pair ou impair; à l'aide d'une table de recherche de taille constante ou d'une table de hachage
O (logn) - trouver un élément dans un tableau trié avec une recherche binaire
O (n) - recherche d'un article dans une liste non triée; ajout de deux numéros à n chiffres
O (N 2 ) - multiplication de deux nombres à n chiffres par un algorithme simple; addition de deux matrices n×n; tri de bulles ou tri d'insertion
O (N 3 ) - multiplication de deux matrices n×n par un algorithme simple
O (C n ) - trouver la solution (exacte) au problème du vendeur itinérant en utilisant la programmation dynamique; déterminer si deux énoncés logiques sont équivalents en utilisant la force brute
O (N!)- Résoudre le problème du vendeur itinérant par la recherche de la force brute
O (N n ) - souvent utilisé au lieu de O (N!) pour obtenir des formules plus simples pour la complexité asymptotique
petit rappel: la notation big O
est utilisée pour désigner complexité asymptotique (c'est-à-dire lorsque la taille du problème croît à l'infini), et il cache une constante.
cela signifie qu'entre un algorithme en O(n) et un algorithme en O(N 2 ), le plus rapide n'est pas toujours le premier (bien qu'il existe toujours une valeur de n telle que pour les problèmes de taille >n, le premier algorithme est le la plus rapide).
notez que la constante cachée dépend beaucoup de l'implémentation!
aussi, dans certains cas, la durée d'exécution n'est pas une fonction déterministe de la" taille n de l'entrée. Prenez le tri en utilisant le tri rapide par exemple: le temps nécessaire pour trier un tableau d'éléments n n'est pas une constante mais dépend de la configuration de départ du tableau.
Il y a différents temps complexités:
- Pire des cas (généralement le plus simple à comprendre, mais pas toujours très significatif)
-
cas Moyen (généralement beaucoup plus difficile à comprendre...)
-
...
une bonne introduction est une Introduction à l'analyse des algorithmes par R. Sedgewick et P. Flajolet.
comme vous dites, premature optimisation is the root of all evil
, et (si possible) profilage vraiment devrait toujours être utilisé lors de l'optimisation du code. Il peut même vous aider à déterminer la complexité des algorithmes.
voir les réponses ici, je pense que nous pouvons conclure que la plupart d'entre nous se rapprochent en effet de l'ordre de l'algorithme par en regardant et en utilisant le bon sens au lieu de le calculer avec, par exemple, le méthode de maître comme nous avons été pensé à l'Université. Avec cela dit, je dois ajouter que même le professeur nous a encouragés (plus tard) pour réellement penser au lieu de simplement calculer.
aussi je voudrais ajouter comment il est fait pour fonctions récursives :
supposons que nous ayons une fonction comme ( scheme code ):
(define (fac n)
(if (= n 0)
1
(* n (fac (- n 1)))))
qui calcule de façon récursive le facteur du nombre donné.
la première étape est d'essayer de déterminer la caractéristique de performance pour le corps de la fonction seulement dans ce cas, rien de spécial est fait dans le corps, juste une multiplication (ou le retour de la valeur 1).
ainsi la performance pour le corps est: O(1) (constant).
essai Suivant et déterminez ceci pour le nombre d'appels récursifs . Dans ce cas, nous avons des appels récursifs n-1.
ainsi la performance pour les appels récursifs est: O (n-1) (l'ordre est n, Comme nous jetons l'insignifiant partie.)
alors mettez ces deux-là ensemble et vous avez alors la performance pour l'ensemble de la fonction récursive:
1 * (n-1) = O (n)
Peter , pour répondre vos questions soulevées; la méthode que je décris ici traite en fait assez bien. Mais gardez à l'esprit qu'il s'agit toujours d'une approximation et non une réponse mathématiquement correcte. La méthode décrite ici est aussi l'une des méthodes que nous avons enseignées à l'université, et si je me souviens bien a été utilisé pour des algorithmes beaucoup plus avancés que le factoriel que j'ai utilisé dans cet exemple.
Bien sûr, tout dépend de la façon dont vous pouvez estimer le temps d'exécution du corps de la fonction et le nombre d'appels récursifs, mais qui est tout aussi vrai pour les autres méthodes.
si votre coût est un polynôme, il suffit de garder le terme d'ordre le plus élevé, sans son multiplicateur. Par exemple:
O((n/2 + 1)*(n/2)) = O(n 2 /4 + n/2) = O(n 2 /4) = O(n 2 )
ça ne marche pas pour les séries infinies. Il n'y a pas de recette unique pour le cas général, bien que pour certains cas courants, les inégalités suivantes s'appliquent:
O(log N ) < O( N ) < O( N journal N ) < O( N 2 ) < O( N k ) < O(e n ) < O( n !)
j'y pense en termes d'information. Tout problème consiste à apprendre un certain nombre de bits.
votre outil de base est le concept de points de décision et leur entropie. L'entropie d'un point de décision est l'information moyenne qu'il vous donnera. Par exemple, si un programme contient un point de décision avec deux branches, son entropie est la somme de la probabilité de chaque branche multipliée par le log 2 de la probabilité inverse de celle direction. C'est ce qu'on apprend en exécutant cette décision.
Par exemple, un if
déclaration ayant deux branches, à la fois tout aussi probable, a une entropie de 1/2 * log(2/1) + 1/2 * journal(2/1) = 1/2 * 1 + 1/2 * 1 = 1. Son entropie est donc de 1 bit.
supposez que vous recherchez une table de N articles, comme N=1024. C'est un problème à 10 bits car log(1024) = 10 bits. Donc, si vous pouvez rechercher SI les états qui ont des résultats équiprobables, il devrait prenez 10 décisions.
C'est ce qu'on obtient avec la recherche binaire.
supposez que vous faites une recherche linéaire. Vous regardez le premier élément et lui demander si c'est celui que vous voulez. Les probabilités sont 1/1024 qu'il est, et 1023/1024 qu'il ne l'est pas. L'entropie de cette décision est 1/1024*log(1024/1) + 1023/1024 * journal(1024/1023) = 1/1024 * 10 + 1023/1024 * sur 0 = environ .01 bits. Vous avez appris très peu de choses! La deuxième décision n'est pas meilleure. C'est pourquoi la recherche linéaire est si lent. En fait, c'est exponentiel dans le nombre de bits que vous devez apprendre.
supposez que vous faites de l'indexation. Supposons que la table est pré-triée dans beaucoup de bacs, et vous utilisez certains de tous les bits dans la clé pour indexer directement à l'entrée de table. Si il y a 1024 bacs, l'entropie est 1/1024 * log(1024) + 1/1024 * log(1024) + ... pour tous les 1024 résultats possibles. C'est 1/1024 * 10 fois 1024 résultats, ou 10 bits d'entropie pour cette indexation opération. C'est pourquoi la recherche par indexage est rapide.
pensez maintenant au tri. Vous avez N articles, et vous avez une liste. Pour chaque élément, vous devez rechercher où l'élément va dans la liste, puis l'ajouter à la liste. Ainsi, le tri prend environ N fois le nombre d'étapes de la recherche sous-jacente.
donc tries basées sur des décisions binaires ayant à peu près les mêmes résultats probables tous prendre environ O(N log n) pas. Un algorithme de Tri O(N) est possible s'il est basé sur l'indexation de la recherche.
j'ai trouvé que presque tous les problèmes de performance algorithmique peuvent être considérés de cette façon.
commençons par le début.
tout d'Abord, accepter le principe que certaines opérations simples sur des données peut être fait dans O(1)
le temps, c'est, dans le temps qui est indépendant de la taille de l'entrée. Ces opérations primitives en Do consistent en
- opérations arithmétiques (p.ex. + ou %).
- opérations Logiques (par exemple, &&).
- opérations de comparaison (par exemple, <=).
- opérations d'accès à la Structure (p.ex. indexage de tableaux comme un[i], ou pointeur fol- la diminution avec l' -> opérateur).
- Simple affectation telle que la copie d'une valeur dans une variable.
- appelle à des fonctions de bibliothèque (par exemple, scanf, printf).
la justification de ce principe exige une étude détaillée des instructions de la machine (étapes primitives) d'un ordinateur typique. Chaque décrit les opérations peuvent être effectuées avec un nombre limité d'instructions machine; souvent seulement un ou deux instructions sont nécessaires.
En conséquence, plusieurs types d'instructions en C peuvent être exécutées en temps O(1)
, c'est-à-dire en un temps constant indépendant de l'entrée. Il s'agit notamment de
- instructions d'Affectation qui n'impliquent pas les appels de fonction dans leurs expressions.
- lire les déclarations.
- écrire des énoncés qui n'exigent pas d'appels de fonction pour évaluer les arguments.
- Le saut états pause, continuer, goto, et le retour de l'expression, où l'expression ne contient pas d'appel de fonction.
en C, beaucoup de for-loops se forment en initialisant une variable d'indice à une certaine valeur et incrémenter cette variable de 1 à chaque fois autour de la boucle. Le pour-boucle se termine lorsque l'indice atteint une certaine limite. Par exemple, le for-loop
for (i = 0; i < n-1; i++)
{
small = i;
for (j = i+1; j < n; j++)
if (A[j] < A[small])
small = j;
temp = A[small];
A[small] = A[i];
A[i] = temp;
}
utilise la variable d'indice I. Il augmente i par 1 à chaque fois autour de la boucle, et les itérations arrêtez - vous quand j'atteins la n-1.
Cependant, pour le moment, se concentrer sur la forme simple de for-loop, où la différence entre les valeurs finales et initiales, divisé par le montant par lequel la variable d'indice est incrémentée nous dit combien de fois nous allons autour de la boucle . Ce compte est exact, à moins qu'il n'y ait des moyens de sortir de la boucle via une instruction de saut; c'est une limite supérieure sur le nombre d'itérations dans tous les cas.
par exemple, le for-loop itérates ((n − 1) − 0)/1 = n − 1 times
,
puisque 0 est la valeur initiale de i, n-1 est la valeur la plus élevée atteinte par i (i.e., quand i
atteint n-1, la boucle s'arrête et aucune itération se produit avec i = n-1), et 1 est ajouté
de i à chaque itération de la boucle.
dans le cas le plus simple, où le temps passé dans le corps de boucle est le même pour chaque itération, on peut multiplier le grand-oh limite supérieure du corps par le nombre de temps autour de la boucle . Strictement parlant ,nous devons alors ajouter O (1) temps pour initialiser l'indice de boucle et O(1) pour la première comparaison de l'indice de boucle avec le limite , parce que nous testons une fois de plus que nous faisons le tour de la boucle. Cependant, à moins que il est possible d'exécuter la boucle zéro fois, le temps d'initialiser la boucle et test la limite d'une fois est d'ordre faible terme peut être abandonné par la règle de sommation.
considérons maintenant cet exemple:
(1) for (j = 0; j < n; j++)
(2) A[i][j] = 0;
Nous savons que en ligne (1) prend O(1)
du temps. Clairement, nous faisons le tour de la boucle n fois, comme
nous pouvons déterminer en soustrayant la limite inférieure de la limite supérieure trouvée sur la ligne
(1), puis en ajoutant 1. Puisque le corps, line (2), prend O(1) Temps, nous pouvons négliger
temps pour incrémenter j, et le temps de comparer les j avec n, qui sont tous deux également en O(1).
Ainsi, le temps de fonctionnement des lignes (1) et (2) est le produit de n et O(1) , qui est O(n)
.
de même, nous pouvons lier le temps de fonctionnement de la boucle extérieure constituée de lignes (2) à (4), qui est
(2) for (i = 0; i < n; i++)
(3) for (j = 0; j < n; j++)
(4) A[i][j] = 0;
nous avons déjà établi que la boucle des lignes (3) et (4) prend du temps O(n). Ainsi, on peut négliger le temps O(1) pour incrémenter i et tester si i < n dans chaque itération, concluant que chaque itération de la boucle externe prend O (n) temps.
l'initialisation i = 0 de la boucle extérieure et l'essai (n + 1)st de la condition
de même, je prends du temps et je peux être négligé. Enfin, nous observons que nous allons
autour de la boucle extérieure n temps, prenant O (n) temps pour chaque itération, donnant un total
O(n^2)
durée.
un exemple plus pratique.
si vous voulez estimer l'ordre de votre code empiriquement plutôt qu'en analysant le code, vous pouvez coller dans une série de valeurs croissantes de n et de temps votre code. Calculez vos horaires sur une échelle de log. Si le code est O (X^n), Les valeurs doivent tomber sur une ligne de pente N.
cela présente plusieurs avantages par rapport à l'étude du code. Pour une chose, vous pouvez voir si vous êtes dans la gamme où le temps de course approche son ordre asymptotique. Aussi, vous pouvez trouver qu'un code que vous pensiez être de l'ordre O(x) est vraiment de l'ordre O (x^2), par exemple, à cause du temps passé dans les appels de la bibliothèque.
ce qui arrive 90% du temps, C'est l'analyse des boucles. Vous avez des boucles simples, doubles, triples? Le temps que vous avez O(n), O(N^2), O (N^3).
très rarement (sauf si vous écrivez une plate-forme avec une bibliothèque de base étendue (comme par exemple, la BCL.net, ou la STL de C++) vous rencontrerez quelque chose qui est plus difficile que de simplement regarder vos boucles (pour les déclarations, while, goto, etc...)
décomposez l'algorithme en morceaux pour lesquels vous connaissez la notation big O, et combinez les opérateurs big O. C'est la seule façon que je connaisse.
pour plus d'informations, consultez la page Wikipedia sur le sujet.
la notation Big O est utile parce qu'elle est facile à utiliser et cache des complications et des détails inutiles (pour une certaine définition d'inutile). Une bonne façon de comprendre la complexité des algorithmes divide and conquer est la méthode tree. Disons que vous avez une version de quicksort avec la procédure médiane, donc vous divisez le tableau en subarrays parfaitement équilibrés à chaque fois.
construisez maintenant un arbre correspondant à tous les tableaux avec lesquels vous travaillez. À la racine, vous avoir le tableau d'origine, la racine a deux enfants qui sont les sous-tableaux. Répétez ceci jusqu'à ce que vous ayez des tableaux d'éléments simples en bas.
puisque nous pouvons trouver la médiane dans le temps O(n) et diviser le tableau en deux parties dans le temps O(n), le travail fait à chaque noeud est O(k) où k est la taille du tableau. Chaque niveau de l'arbre contient (au plus) le tableau entier de sorte que le travail par niveau est O (n) (les tailles des subarrays additionnent jusqu'à n, et puisque nous avons O (k) Par niveau nous pouvons ajouter cet). Il n'y a que des niveaux de log(N) dans l'arbre depuis chaque fois que nous divisons l'entrée en deux.
nous pouvons donc limiter la quantité de travail par O(N*log(n)).
cependant, Big O cache quelques détails que nous ne pouvons pas toujours ignorer. Considérer le calcul de la séquence de Fibonacci avec
a=0;
b=1;
for (i = 0; i <n; i++) {
tmp = b;
b = a + b;
a = tmp;
}
et supposons que les a et b sont des BigIntegers en Java ou quelque chose qui peut gérer arbitrairement de grands nombres. La plupart des gens supposons qu'il s'agisse d'un algorithme O(n) sans hésitation. Le raisonnement est que vous avez n itérations dans la boucle for et O(1) travaillent dans le côté de la boucle.
mais les nombres de Fibonacci sont grands, le nombre de Fibonacci n-th est exponentiel en n donc juste le stocker prendra de l'ordre de n bytes. Effectuer l'addition avec de grands entiers prendra O (n) quantité de travail. Ainsi, la quantité totale de travail effectué dans cette procédure est de
1 + 2 + 3 + ... + n = n (n-1) / 2 = O (N^2)
donc cet algorithme tourne en quadradic time!
familiarité avec les algorithmes/structures de données que j'utilise et/ou Analyse rapide de l'imbrication des itérations. La difficulté est lorsque vous appelez une fonction de bibliothèque, peut - être plusieurs fois-vous pouvez souvent être incertain de savoir si vous appelez la fonction inutilement à certains moments ou quelle implémentation ils utilisent. Peut-être que les fonctions de bibliothèque devraient avoir une mesure de complexité/efficacité, que ce soit Big O ou une autre métrique, qui est disponible dans la documentation ou même IntelliSense .
moins utile en général, je pense , mais par souci d'exhaustivité il y a aussi un Big Omega Ω , qui définit une limite inférieure sur la complexité d'un algorithme , et un Big Theta Θ , qui définit à la fois une limite supérieure et inférieure.
Comme "comment voulez-vous calculer le" Big O, c'est en partie de théorie de la complexité Computationnelle . Pour certains (beaucoup) cas spéciaux, vous pouvez être en mesure de venir avec quelques heuristiques simples (comme la multiplication des nombres de boucles pour les boucles imbriquées), esp. quand tout ce que vous voulez est une estimation limite supérieure, et vous ne vous souciez pas si elle est trop pessimiste - qui je suppose est probablement ce que votre question Est au sujet.
Si vous voulez vraiment répondre à votre question de tout l'algorithme le mieux que vous pouvez faire est d'appliquer la théorie. En plus de l'analyse "worst case" simpliste, j'ai trouvé Amortized analysis très utile dans la pratique.
pour le premier cas, la boucle intérieure est exécutée n-i
fois, de sorte que le nombre total d'exécutions est la somme de i
allant de 0
à n-1
(parce que Inférieur, Pas inférieur ou égal) du n-i
. Vous obtenez finalement n*(n + 1) / 2
, donc O(n²/2) = O(n²)
.
pour la 2ème boucle, i
est compris entre 0
et n
inclus pour la boucle extérieure; alors la boucle intérieure est exécutée lorsque j
est strictement supérieur à n
, ce qui est impossible.
en plus d'utiliser la méthode master (ou l'une de ses spécialisations), je teste mes algorithmes expérimentalement. Cela ne peut pas prouver que n'importe quelle classe de complexité particulière est atteinte, mais il peut fournir l'assurance que l'analyse mathématique est appropriée. Pour aider à rassurer, j'utilise des outils de couverture codée en conjonction avec mes expériences, pour m'assurer que j'exerce tous les cas.
comme un exemple très simple vous disent je voulais vérifier la vitesse de la liste du framework. net. Vous pouvez écrire quelque chose comme ce qui suit, puis analyser les résultats dans Excel pour s'assurer qu'ils ne dépassent pas une courbe n*log(n).
dans cet exemple, je mesure le nombre de comparaisons, mais il est également prudent d'examiner le temps réel requis pour chaque taille d'échantillon. Cependant, vous devez être encore plus prudent que vous ne faites que mesurer l'algorithme et ne pas inclure les artefacts de votre test infrastructure.
int nCmp = 0;
System.Random rnd = new System.Random();
// measure the time required to sort a list of n integers
void DoTest(int n)
{
List<int> lst = new List<int>(n);
for( int i=0; i<n; i++ )
lst[i] = rnd.Next(0,1000);
// as we sort, keep track of the number of comparisons performed!
nCmp = 0;
lst.Sort( delegate( int a, int b ) { nCmp++; return (a<b)?-1:((a>b)?1:0)); }
System.Console.Writeline( "{0},{1}", n, nCmp );
}
// Perform measurement for a variety of sample sizes.
// It would be prudent to check multiple random samples of each size, but this is OK for a quick sanity check
for( int n = 0; n<1000; n++ )
DoTest(n);
n'oubliez pas de tenir compte également des complexités spatiales qui peuvent également être une source de préoccupation si l'on dispose de ressources mémoire limitées. Ainsi, par exemple, vous pouvez entendre quelqu'un vouloir un algorithme d'espace constant qui est fondamentalement une façon de dire que la quantité d'espace pris par l'algorithme ne dépend pas de facteurs à l'intérieur du code.
parfois la complexité peut venir de combien de fois est quelque chose appelé, combien de fois est une boucle exécutée, combien de fois est la mémoire il s'agit d'une question qui est à la fois une question de principe et une question de procédure.
enfin, big O peut être utilisé pour le pire des cas, le meilleur des cas et les cas d'amortissement où c'est généralement le pire des CAs qui est utilisé pour décrire à quel point un algorithme peut être mauvais.
ce qui est souvent négligé est le comportement attendu de vos algorithmes. il ne change pas le Big-O de votre algorithme , mais il se rapporte à la déclaration " optimisation prématurée. . .."
comportement Attendu de votre algorithme est -- très abrutir -- à quelle vitesse vous pouvez attendre de votre algorithme de travailler sur des données qui vous êtes le plus susceptible de voir.
par exemple, si vous cherchez une valeur dans une liste, C'est O(n), mais si vous savez que la plupart des listes que vous voyez ont votre valeur à l'avance, le comportement typique de votre algorithme est plus rapide.
pour vraiment le clouer vers le bas, vous devez être en mesure de décrire la distribution de probabilité de votre "espace d'entrée" (si vous avez besoin de trier une liste, combien de fois cette liste va déjà être triée? combien de fois est-il totalement inversée? à quelle fréquence est-il le plus souvent trié? Il n'est pas toujours possible que vous le sachiez, mais parfois vous le savez.
grande question!
si vous utilisez le Big O, vous parlez du pire cas (plus sur ce que cela signifie plus tard). En outre, il ya capital theta pour le cas moyen et un grand omega pour le meilleur cas.
consultez ce site pour une belle définition formelle de Big O: https://xlinux.nist.gov/dads/HTML/bigOnotation.html
f (n) = O (g (n)) signifie qu'il y a des constantes positives c et k, de telle sorte que 0 ≤ f(n) ≤ cg(n) Pour tout n ≥ k. Les valeurs de c et k doivent être fixées pour la fonction f et ne doivent pas dépendre de N.
Ok, alors qu'entendons-nous par "meilleure hypothèse" et "pire hypothèse"?
ceci est probablement illustré le plus clairement par des exemples. Par exemple, si nous employons la recherche linéaire pour trouver un nombre dans un tableau trié alors le le pire cas est quand nous décider de recherche pour le dernier élément de la matrice que cela prendrait autant d'étapes qu'il y a d'éléments dans le tableau. Le meilleur cas serait lorsque nous cherchons le premier élément puisque nous serions fait après la première vérification.
Le point de toutes ces adjectif -la complexité, c'est que nous sommes à la recherche d'un moyen pour tracer le graphique de la quantité de temps un hypothétique programme va jusqu'à l'achèvement en termes de la taille des variables particulières. Toutefois, pour de nombreux algorithmes, vous pouvez faire valoir qu'il n'y a pas un seul moment pour une taille particulière de l'entrée. Notez que ceci est en contradiction avec l'exigence fondamentale d'une fonction, n'importe quelle entrée doit pas avoir plus d'une sortie. Donc nous arrivons avec multiples fonctions pour décrire la complexité d'un algorithme. Maintenant, même si la recherche d'un tableau de taille n peut prendre des quantités variables de temps en fonction de ce vous cherchez dans le tableau et en fonction de la proportion de n, Nous pouvons créer une description informative de l'algorithme en utilisant les classes du meilleur cas, du cas moyen et du pire cas.
Désolé, c'est tellement mal écrit et n'a pas beaucoup d'informations techniques. Mais j'espère que ça rendra les cours de complexité temporelle plus faciles à penser. Une fois que vous devenez à l'aise avec ceux-ci, il devient une simple question de parsing à travers votre programme et à la recherche de choses comme pour-boucles qui dépendent sur les tailles de tableau et le raisonnement basé sur vos structures de données quel type d'entrée résulterait dans les cas triviaux et quelle entrée résulterait dans les pires cas.
pour le code A, la boucle extérieure s'exécute pour n+1 fois, le " 1 " temps est le processus qui vérifie si je respecte toujours l'exigence. Et la boucle interne tourne n fois, n-2 fois.... Ainsi, 0+2+..+(n-2)+n= (0+n) (n+1)/2= O(n2).
pour le code B, bien qu'inner loop n'intervienne pas et n'exécute pas foo (), la boucle interne sera exécutée pour n fois dépend du temps d'exécution de la boucle externe, qui est O (n)
Je ne sais pas comment le résoudre programmatiquement, mais la première chose que les gens font est que nous échantillonnons l'algorithme pour certains modèles dans le nombre d'opérations effectuées, disons 4N^2 + 2n + 1 nous avons 2 règles:
- si nous avons une somme de termes, le terme ayant le taux de croissance le plus élevé est conservé, les autres termes étant omis.
- Si nous avons un produit de plusieurs facteurs facteurs constants sont omis.
si nous simplifions f(x), où f(x) est la formule pour le nombre d'opérations effectuées, (4n^2 + 2n + 1 expliqué ci-dessus), nous obtenons la valeur big-O [O(N^2) dans ce cas]. Mais cela devrait tenir compte de l'interpolation de Lagrange dans le programme, qui peut être difficile à mettre en œuvre. Et si la vraie valeur de big-O était O (2^N), et que nous avions quelque chose comme O(x^n), donc cet algorithme ne serait probablement pas programmable. Mais si quelqu'un prouve que j'ai tort, donnez-moi le code . . . .