La plupart des personnes ayant un diplôme en CS certainement savoir ce que signifie Big O pour . Il nous aide à mesurer l'efficacité d'un algorithme.
Mais je suis curieux, comment calculez- vous ou approximez-vous la complexité de vos algorithmes?
La plupart des personnes ayant un diplôme en CS certainement savoir ce que signifie Big O pour . Il nous aide à mesurer l'efficacité d'un algorithme.
Mais je suis curieux, comment calculez- vous ou approximez-vous la complexité de vos algorithmes?
Réponses:
Je ferai de mon mieux pour l'expliquer ici en termes simples, mais sachez que ce sujet prend quelques mois à mes élèves pour enfin comprendre. Vous pouvez trouver plus d'informations sur le chapitre 2 du livre Structures de données et algorithmes en Java .
Aucune procédure mécanique ne peut être utilisée pour obtenir le BigOh.
En tant que «livre de recettes», 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 le nombre d'étapes de calcul exécutées à partir d'une entrée d'une certaine taille.
Le but est simple: comparer des algorithmes d'un point de vue théorique, sans avoir besoin d'exécuter le code. Plus le nombre d'étapes est petit, plus l'algorithme est rapide.
Par exemple, supposons que vous ayez ce morceau de code:
Cette fonction renvoie la somme de tous les éléments du tableau, et nous voulons créer une formule pour compter la complexité de calcul de cette fonction:
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 telle que:Le paramètre
N
prend ladata.length
valeur. Maintenant, nous avons besoin de la définition réelle de la fonctionf()
. Cela se fait à partir du code source, dans lequel chaque ligne intéressante est numérotée de 1 à 4.Il existe de nombreuses façons de calculer le BigOh. À partir de ce point, nous allons supposer que chaque phrase qui ne dépend pas de la taille des données d'entrée prend un
C
nombre de pas de calcul constant .Nous allons ajouter le nombre individuel d'étapes de la fonction, et ni la déclaration de variable locale ni l'instruction de retour ne dépendent de la taille du
data
tableau.Cela signifie que les lignes 1 et 4 prennent chacune un nombre C d'étapes, et la fonction est un peu comme ceci:
La partie suivante consiste à définir la valeur de l'
for
instruction. N'oubliez pas que nous comptons le nombre d'étapes de calcul, ce qui signifie que le corps de l'for
instruction est exécutéN
fois. C'est la même chose que d'ajouterC
,N
fois:Il n'y a pas de règle mécanique pour compter le nombre de fois où le corps de l'objet
for
est exécuté, vous devez le compter en regardant ce que fait le code. Pour simplifier les calculs, nous ignorons les parties d'initialisation, de condition et d'incrémentation variables de l'for
instruction.Pour obtenir le BigOh réel, nous avons besoin de l' analyse asymptotique de la fonction. Cela se fait grosso modo comme ceci:
C
.f()
obtenir le polynôme dans sonstandard form
.N
approcheinfinity
.Notre
f()
a deux termes:Suppression de toutes les
C
constantes et parties redondantes:Puisque le dernier terme est celui qui grossit à l'
f()
approche de l'infini (pensez aux limites ), c'est l'argument BigOh, et lasum()
fonction a un BigOh de:Il existe quelques astuces pour résoudre certaines astuces: utilisez des sommations chaque fois que vous le pouvez.
Par exemple, ce code peut être facilement résolu à l'aide de sommations:
La première chose qu'il fallait vous demander, c'est l'ordre d'exécution de
foo()
. Bien que l'habituel soit le casO(1)
, vous devez interroger vos professeurs à ce sujet.O(1)
signifie (presque, principalement) constantC
, indépendamment de la tailleN
.La
for
déclaration sur la phrase numéro un est délicate. Pendant que l'index se termine à2 * N
, l'incrémentation se fait par deux. Cela signifie que la premièrefor
n'est exécutée que parN
étapes, et nous devons diviser le nombre par deux.La phrase numéro deux est encore plus délicate car elle dépend de la valeur de
i
. Jetez un œil: l'indice i prend les valeurs: 0, 2, 4, 6, 8, ..., 2 * N, et le secondfor
s'exécute: N fois le premier, N - 2 le second, N - 4 le troisième ... jusqu'au stade N / 2, sur lequel le secondfor
n'est jamais exécuté.Sur la formule, cela signifie:
Encore une fois, nous comptons le nombre d'étapes . Et par définition, chaque sommation doit toujours commencer à un et se terminer à un nombre supérieur ou égal à un.
(Nous supposons que
foo()
c'estO(1)
et prend desC
mesures.)Nous avons un problème ici: lorsque
i
prend la valeurN / 2 + 1
vers le haut, la sommation intérieure se termine par un nombre négatif! C'est impossible et faux. Nous devons diviser la somme en deux, étant le point central du momenti
prendN / 2 + 1
.Depuis le moment charnière
i > N / 2
, l'intérieurfor
ne sera pas exécuté, et nous supposons une complexité d'exécution C constante sur son corps.Désormais, les sommations peuvent être simplifiées à l'aide de quelques règles d'identité:
w
)Appliquer une algèbre:
Et le BigOh c'est:
la source
O(n)
oùn
est le nombre d'éléments, ouO(x*y)
oùx
ety
sont les dimensions du tableau. Big-oh est "par rapport à l'entrée", cela dépend donc de votre entrée.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 ensembles de données (listes) mais peut être utilisé ailleurs.
Quelques exemples de son utilisation dans le code C.
Disons que nous avons un tableau de n éléments
Si nous voulions accéder au premier élément du tableau, ce serait O (1) car peu importe la taille du tableau, il faut toujours le même temps constant pour obtenir le premier élément.
Si nous voulions trouver un numéro dans la liste:
Ce serait O (n) car il faudrait tout au plus parcourir toute la liste 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 parcourir la boucle une fois parce que Big-O décrit la limite supérieure d'un algorithme (les oméga sont pour la borne inférieure et thêta pour la borne étroite) .
Lorsque nous arrivons aux boucles imbriquées:
Il s'agit de O (n ^ 2) car pour chaque passage de la boucle externe (O (n)), nous devons parcourir à nouveau la liste entière, de sorte que les n se multiplient, nous laissant avec n au carré.
C'est à peine gratter la surface, mais lorsque vous arrivez à analyser des algorithmes plus complexes, des mathématiques complexes impliquant des preuves entrent en jeu. J'espère que cela vous familiarise au moins avec les bases.
la source
O(1)
travail eux - mêmes. Dans les API standard C par exemple,bsearch
est intrinsèquementO(log n)
,strlen
estO(n)
etqsort
estO(n log n)
(techniquement, il n'a aucune garantie, et le tri rapide lui-même a une complexité de pire casO(n²)
, mais en supposant que votrelibc
auteur n'est pas un idiot, sa complexité de cas moyenne estO(n log n)
et il utilise une stratégie de sélection de pivot qui réduit les chances de frapper leO(n²)
cas). Et les deuxbsearch
etqsort
peuvent être pires si la fonction de comparaison est pathologique.Bien qu'il soit utile de savoir comment déterminer le temps Big O 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, extraits 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) - Recherche d'un élément dans un tableau trié avec une recherche binaire
O (n) - Recherche d'un élément dans une liste non triée; addition de deux nombres à n chiffres
O (n 2 ) - Multiplier deux nombres à n chiffres par un algorithme simple; addition de deux matrices n × n; tri à bulles ou tri par 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 voyageur de commerce en utilisant la programmation dynamique; déterminer si deux instructions logiques sont équivalentes à l'aide de la force brute
O (n!) - Résolution du problème des vendeurs itinérants via la recherche par force brute
O (n n ) - Souvent utilisé au lieu de O (n!) Pour dériver des formules plus simples de complexité asymptotique
la source
x&1==1
pour vérifier la bizarrerie?x & 1
serait suffisant, pas besoin de vérifier== 1
; en C,x&1==1
est évalué commex&(1==1)
grâce à la priorité de l'opérateur , donc c'est en fait la même chose que le testx&1
). Je pense cependant que vous avez mal interprété la réponse; il y a un point-virgule, pas une virgule. Cela ne signifie pas que vous auriez besoin d'une table de recherche pour les tests pairs / impairs, mais que les tests pairs / impairs et la vérification d'une table de recherche sont desO(1)
opérations.Petit rappel: le
big O
notation est utilisée pour dénoter la complexité asymptotique (c'est-à-dire lorsque la taille du problème augmente à l'infini), et elle cache une constante.Cela signifie qu'entre un algorithme en O (n) et un 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 soit le plus rapide).
Notez que la constante cachée dépend beaucoup de l'implémentation!
De plus, dans certains cas, le runtime n'est pas une fonction déterministe du taille n de l'entrée. Prenez le tri en utilisant le tri rapide par exemple: le temps nécessaire pour trier un tableau de n éléments n'est pas une constante mais dépend de la configuration de départ du tableau.
Il existe différentes complexités temporelles:
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 le dites,
premature optimisation is the root of all evil
et (si possible) le profilage doit toujours être utilisé lors de l'optimisation du code. Il peut même vous aider à déterminer la complexité de vos algorithmes.la source
En voyant les réponses ici, je pense que nous pouvons conclure que la plupart d'entre nous se rapprochent effectivement de l'ordre de l'algorithme en le regardant et en utilisant le bon sens au lieu de le calculer avec, par exemple, la méthode principale comme nous le pensions à l'université. Cela dit, je dois ajouter que même le professeur nous a encouragés (plus tard) à y penser au lieu de simplement le calculer.
J'aimerais également ajouter comment cela se fait pour les fonctions récursives :
supposons que nous ayons une fonction comme ( code de schéma ):
qui calcule récursivement la factorielle du nombre donné.
La première étape consiste à essayer de déterminer la caractéristique de performance pour le corps de la fonction uniquement dans ce cas, rien de spécial n'est fait dans le corps, juste une multiplication (ou le retour de la valeur 1).
Donc, la performance pour le corps est: O (1) (constante).
Essayez ensuite de déterminer cela pour le nombre d'appels récursifs . Dans ce cas, nous avons n-1 appels récursifs.
Donc, la performance pour les appels récursifs est: O (n-1) (l'ordre est n, car nous jetons les parties insignifiantes).
Ensuite, mettez ces deux ensemble et vous avez alors les performances 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 gère cela assez bien. Mais gardez à l'esprit qu'il s'agit toujours d'une approximation et non d'une réponse mathématiquement correcte. La méthode décrite ici est également l'une des méthodes qui nous ont été enseignées à l'université, et si je me souviens bien, elle a été utilisée pour des algorithmes beaucoup plus avancés que la factorielle que j'ai utilisée 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 cela est tout aussi vrai pour les autres méthodes.
la source
Si votre coût est un polynôme, conservez simplement le terme d'ordre le plus élevé, sans son multiplicateur. Par exemple:
Cela ne fonctionne pas pour les séries infinies, faites attention. 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:
la source
J'y pense en termes d'informations. 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 cette branche. C'est tout ce que vous apprenez en exécutant cette décision.
Par exemple, une
if
déclaration ayant deux branches, toutes deux également probables, a une entropie de 1/2 * log (2/1) + 1/2 * log (2/1) = 1/2 * 1 + 1/2 * 1 = 1. Son entropie est donc de 1 bit.Supposons que vous recherchez un tableau de N éléments, comme N = 1024. C'est un problème de 10 bits car log (1024) = 10 bits. Donc, si vous pouvez le rechercher avec des déclarations IF qui ont des résultats tout aussi probables, il devrait prendre 10 décisions.
C'est ce que vous obtenez avec la recherche binaire.
Supposons que vous effectuez une recherche linéaire. Vous regardez le premier élément et demandez si c'est celui que vous voulez. Les probabilités sont de 1/1024 et de 1023/1024. L'entropie de cette décision est 1/1024 * log (1024/1) + 1023/1024 * log (1024/1023) = 1/1024 * 10 + 1023/1024 * environ 0 = environ 0,01 bit. Tu as très peu appris! La deuxième décision n'est pas beaucoup mieux. C'est pourquoi la recherche linéaire est si lente. En fait, c'est exponentiel dans le nombre de bits que vous devez apprendre.
Supposons que vous effectuez l'indexation. Supposons que la table soit pré-triée en un grand nombre de casiers et que vous utilisiez certains de tous les bits de la clé pour indexer directement l'entrée de la table. S'il y a 1024 bins, l'entropie est 1/1024 * log (1024) + 1/1024 * log (1024) + ... pour les 1024 résultats possibles. C'est 1/1024 * 10 fois 1024 résultats, ou 10 bits d'entropie pour cette opération d'indexation. C'est pourquoi l'indexation de la recherche est rapide.
Pensez maintenant au tri. Vous avez N éléments 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. Le tri prend donc environ N fois le nombre d'étapes de la recherche sous-jacente.
Ainsi, les tris basés sur des décisions binaires ayant des résultats à peu près tout aussi probables prennent tous des étapes O (N log N). Un algorithme de tri O (N) est possible s'il est basé sur une recherche d'indexation.
J'ai constaté que presque tous les problèmes de performances algorithmiques peuvent être examinés de cette manière.
la source
Commençons depuis le début.
Tout d'abord, acceptez le principe selon lequel certaines opérations simples sur les données peuvent être effectuées dans le
O(1)
temps, c'est-à-dire dans un temps indépendant de la taille de l'entrée. Ces opérations primitives en C consistent enLa justification de ce principe nécessite une étude détaillée des instructions machine (étapes primitives) d'un ordinateur type. Chacune des opérations décrites peut être effectuée avec un petit nombre d'instructions machine; souvent, une ou deux instructions seulement sont nécessaires. Par conséquent, plusieurs types d'instructions en C peuvent être exécutées en
O(1)
temps, c'est-à-dire en un temps constant indépendant de l'entrée. Ces simples comprennentEn C, de nombreuses boucles for sont formées en initialisant une variable d'index à une certaine valeur et en incrémentant cette variable de 1 à chaque fois dans la boucle. La boucle for se termine lorsque l'index atteint une certaine limite. Par exemple, la boucle for
utilise la variable d'index i. Il incrémente i de 1 à chaque fois dans la boucle, et les itérations s'arrêtent lorsque i atteint n - 1.
Cependant, pour le moment, concentrez-vous sur la forme simple de for-loop, où la différence entre les valeurs finales et initiales, divisée par le montant par lequel la variable d'index est incrémentée, nous indique combien de fois nous parcourons la boucle . Ce décompte est exact, sauf s'il existe des moyens de quitter la boucle via une instruction jump; il s'agit dans tous les cas d'une limite supérieure du nombre d'itérations.
Par exemple, la boucle for itère
((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 (c'est-à-dire que lorsque i atteint n − 1, la boucle s'arrête et aucune itération ne se produit avec i = n− 1), et 1 est ajouté à i à chaque itération de la boucle.Dans le cas le plus simple, où le temps passé dans le corps de la boucle est le même pour chaque itération, nous pouvons multiplier la limite supérieure big-oh pour le corps par le nombre de fois autour de la boucle . À strictement parler, nous devons ensuite ajouter O (1) temps pour initialiser l'index de boucle et O (1) temps pour la première comparaison de l'index de boucle avec la limite , car nous testons une fois de plus que nous faisons le tour de la boucle. Cependant, à moins qu'il soit possible d'exécuter la boucle zéro fois, le temps d'initialiser la boucle et de tester la limite une fois est un terme de faible ordre qui peut être supprimé par la règle de sommation.
Considérez maintenant cet exemple:
Nous savons que la ligne (1) prend du
O(1)
temps. De toute évidence, nous parcourons la boucle n fois, comme nous pouvons le 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, la ligne (2), prend O (1), on peut négliger le temps pour incrémenter j et le temps pour comparer j avec n, tous deux étant également O (1). Ainsi, le temps de parcours des lignes (1) et (2) est le produit de n et O (1) qui estO(n)
.De même, nous pouvons limiter le temps de parcours de la boucle externe composée des lignes (2) à (4), qui est
Nous avons déjà établi que la boucle des lignes (3) et (4) prend le temps O (n). Ainsi, nous pouvons négliger le temps O (1) pour incrémenter i et pour tester si i <n dans chaque itération, concluant que chaque itération de la boucle externe prend le temps O (n).
L'initialisation i = 0 de la boucle externe et le (n + 1) st test de la condition i <n prennent également O (1) et peuvent être négligés. Enfin, nous observons que nous faisons le tour de la boucle externe n fois, en prenant O (n) temps pour chaque itération, ce qui donne un
O(n^2)
temps de fonctionnement total .Un exemple plus pratique.
la source
Si vous souhaitez estimer l'ordre de votre code de manière empirique plutôt qu'en analysant le code, vous pouvez coller une série de valeurs croissantes de n et chronométrer votre code. Tracez vos horaires sur une échelle logarithmique. Si le code est O (x ^ n), les valeurs doivent tomber sur une ligne de pente n.
Cela présente plusieurs avantages par rapport à la simple étude du code. D'une part, vous pouvez voir si vous êtes dans la plage où le temps d'exécution s'approche de son ordre asymptotique. En outre, vous pouvez constater qu'un code que vous pensiez être de l'ordre O (x) est vraiment de l'ordre O (x ^ 2), par exemple, en raison du temps passé dans les appels de bibliothèque.
la source
Fondamentalement, la chose qui survient dans 90% des cas consiste simplement à analyser les boucles. Avez-vous des boucles imbriquées simples, doubles ou triples? Le temps de fonctionnement 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, le .NET BCL ou le STL de C ++), vous rencontrerez tout ce qui est plus difficile que de simplement regarder vos boucles (pour les instructions, tandis que, goto, etc...)
la source
La notation Big O est utile car elle est facile à travailler et cache des complications et des détails inutiles (pour une définition de ce qui est inutile). Une méthode intéressante pour déterminer la complexité des algorithmes de division et de conquête est la méthode de l'arborescence. Supposons que vous ayez une version de quicksort avec la procédure médiane, donc vous divisez le tableau en sous-réseaux parfaitement équilibrés à chaque fois.
Maintenant, construisez un arbre correspondant à tous les tableaux avec lesquels vous travaillez. À la racine, vous avez le tableau d'origine, la racine a deux enfants qui sont les sous-réseaux. Répétez cette opération jusqu'à ce que vous ayez des tableaux d'éléments uniques en bas.
Puisque nous pouvons trouver la médiane en temps O (n) et diviser le tableau en deux parties en temps O (n), le travail effectué à chaque nœud est O (k) où k est la taille du tableau. Chaque niveau de l'arborescence contient (au plus) l'ensemble du tableau, donc le travail par niveau est O (n) (les tailles des sous-réseaux s'additionnent à n, et puisque nous avons O (k) par niveau, nous pouvons l'ajouter) . Il n'y a que des niveaux log (n) dans l'arborescence puisque chaque fois que nous divisons par deux l'entrée.
Par conséquent, nous pouvons limiter la quantité de travail par O (n * log (n)).
Cependant, Big O cache des détails que nous ne pouvons parfois pas ignorer. Envisagez de calculer la séquence de Fibonacci avec
et supposons simplement que a et b sont des BigIntegers en Java ou quelque chose qui peut gérer des nombres arbitrairement grands. La plupart des gens diraient que c'est un algorithme O (n) sans broncher. Le raisonnement est que vous avez n itérations dans la boucle for et O (1) fonctionne à côté de la boucle.
Mais les nombres de Fibonacci sont grands, le n-ième nombre de Fibonacci est exponentiel en n, donc il suffit de le stocker sur l'ordre de n octets. Effectuer l'addition avec de grands nombres entiers nécessitera une quantité de travail O (n). Donc, la quantité totale de travail effectuée dans cette procédure est
1 + 2 + 3 + ... + n = n (n-1) / 2 = O (n ^ 2)
Donc, cet algorithme fonctionne en temps quadradique!
la source
Moins utile en général, je pense, mais dans un souci d'exhaustivité, il existe également un Big Omega Ω , qui définit une limite inférieure de la complexité d'un algorithme, et un Big Theta Θ , qui définit à la fois une limite supérieure et une limite inférieure.
la source
Décomposez l'algorithme en morceaux pour lesquels vous connaissez la notation du grand O et combinez-les via de grands opérateurs O. C'est le seul moyen que je connaisse.
Pour plus d'informations, consultez la page Wikipedia sur le sujet.
la source
Connaissance des algorithmes / structures de données que j'utilise et / ou analyse rapide de l'imbrication d'itérations. La difficulté est lorsque vous appelez une fonction de bibliothèque, éventuellement plusieurs fois - vous pouvez souvent ne pas 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 .
la source
Quant à "comment calculez-vous" Big O, cela fait partie de la théorie de la complexité informatique . Pour certains (nombreux) cas spéciaux, vous pourrez peut-être fournir des heuristiques simples (comme la multiplication des nombres de boucles pour les boucles imbriquées), en particulier. quand tout ce que vous voulez, c'est une estimation de la limite supérieure, et cela ne vous dérange pas si elle est trop pessimiste - ce qui, je suppose, est probablement sur quoi porte votre question.
Si vous voulez vraiment répondre à votre question pour n'importe quel algorithme, le mieux que vous puissiez faire est d'appliquer la théorie. Outre l'analyse simpliste du «pire des cas», j'ai trouvé l' analyse amortie très utile dans la pratique.
la source
Pour le 1er cas, la boucle interne est exécutée
n-i
fois, donc le nombre total d'exécutions est la somme pouri
aller de0
àn-1
(car inférieur à, non inférieur ou égal) de lan-i
. Vous obtenez enfinn*(n + 1) / 2
, alorsO(n²/2) = O(n²)
.Pour la 2e boucle,
i
est entre0
etn
inclus pour la boucle externe; alors la boucle interne est exécutée lorsquej
est strictement supérieur àn
, ce qui est alors impossible.la source
En plus d'utiliser la méthode master (ou l'une de ses spécialisations), je teste expérimentalement mes algorithmes. Cela ne peut pas prouver qu'une classe de complexité particulière est atteinte, mais cela peut rassurer que l'analyse mathématique est appropriée. Pour aider avec cette assurance, j'utilise des outils de couverture de code en conjonction avec mes expériences, pour m'assurer que j'exerce tous les cas.
À titre d'exemple très simple, disons que vous vouliez effectuer un contrôle d'intégrité sur la vitesse de tri de la liste du framework .NET. Vous pouvez écrire quelque chose comme ceci, puis analyser les résultats dans Excel pour vous 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 faire encore plus attention à ne mesurer que l'algorithme et à ne pas inclure les artefacts de votre infrastructure de test.
la source
N'oubliez pas de tenir compte également des complexités spatiales qui peuvent également être une source de préoccupation si les ressources mémoire sont limitées. Ainsi, par exemple, vous pouvez entendre quelqu'un vouloir un algorithme à espace constant, ce qui est essentiellement une façon de dire que la quantité d'espace prise par l'algorithme ne dépend d'aucun facteur à l'intérieur du code.
Parfois, la complexité peut provenir du nombre de fois que quelque chose est appelé, de la fréquence à laquelle une boucle est exécutée, de la fréquence d'allocation de la mémoire, etc. est une autre partie pour répondre à cette question.
Enfin, le grand O peut être utilisé pour le pire des cas, le meilleur des cas et les cas d'amortissement où, généralement, c'est le pire des cas qui est utilisé pour décrire la gravité d'un algorithme.
la source
Ce qui est souvent négligé, c'est le comportement attendu de vos algorithmes. Cela ne change pas le Big-O de votre algorithme , mais il se rapporte à la déclaration "optimisation prématurée ..."
Le comportement attendu de votre algorithme est - très stupide - à quelle vitesse vous pouvez vous attendre à ce que votre algorithme fonctionne sur les données que vous êtes le plus susceptible de voir.
Par exemple, si vous recherchez une valeur dans une liste, c'est O (n), mais si vous savez que la plupart des listes que vous voyez ont votre valeur d'avance, le comportement typique de votre algorithme est plus rapide.
Pour vraiment le comprendre, vous devez être en mesure de décrire la distribution de probabilité de votre "espace d'entrée" (si vous devez trier une liste, à quelle fréquence cette liste sera-t-elle déjà triée? À quelle fréquence est-elle totalement inversée? Comment est-il souvent trié?) Ce n'est pas toujours faisable que vous le sachiez, mais parfois vous le savez.
la source
bonne question!
Avertissement: cette réponse contient de fausses déclarations voir les commentaires ci-dessous.
Si vous utilisez le Big O, vous parlez du pire des cas (plus sur ce que cela signifie plus tard). De plus, il y a du thêta capital pour le cas moyen et un gros oméga pour le meilleur cas.
Consultez ce site pour une belle définition formelle de Big O: https://xlinux.nist.gov/dads/HTML/bigOnotation.html
Ok, alors maintenant qu'est-ce que nous entendons par les complexités «meilleur cas» et «pire cas»?
Ceci est probablement illustré le plus clairement par des exemples. Par exemple, si nous utilisons la recherche linéaire pour trouver un nombre dans un tableau trié, le pire des cas est lorsque nous décidons de rechercher le dernier élément du tableau, car cela prendrait autant d'étapes qu'il y a d'éléments dans le tableau. Le meilleur cas serait lorsque nous recherchons le premier élément puisque nous aurions terminé après la première vérification.
Le point de toutes ces complexités adjectif-cas est que nous recherchons un moyen de représenter graphiquement la durée d'un programme hypothétique jusqu'à la fin en termes de taille de variables particulières. Cependant, pour de nombreux algorithmes, vous pouvez affirmer qu'il n'y a pas une seule fois pour une taille d'entrée particulière. Notez que cela contredit l'exigence fondamentale d'une fonction, toute entrée ne doit pas avoir plus d'une sortie. Nous proposons donc plusieurs fonctions pour décrire la complexité d'un algorithme. Maintenant, même si la recherche d'un tableau de taille n peut prendre plus ou moins de temps en fonction de ce que vous recherchez dans le tableau et en fonction de n, nous pouvons créer une description informative de l'algorithme en utilisant le meilleur des cas, le cas moyen et les classes les plus défavorables.
Désolé, c'est tellement mal écrit et il manque beaucoup d'informations techniques. Mais j'espère que cela facilitera la réflexion sur les classes de complexité temporelle. Une fois que vous vous êtes familiarisé avec celles-ci, il devient simple d'analyser votre programme et de rechercher des éléments tels que des boucles for qui dépendent de la taille des tableaux et du raisonnement en fonction de vos structures de données. Quel type d'entrée entraînerait des cas triviaux et quelle entrée dans les pires cas.
la source
Je ne sais pas comment résoudre ce problème par programme, 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 simplifions f (x), où f (x) est la formule du 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 big-O était O (2 ^ n), et nous pourrions avoir quelque chose comme O (x ^ n), donc cet algorithme ne serait probablement pas programmable. Mais si quelqu'un me prouve le contraire, donnez-moi le code. . . .
la source
Pour le code A, la boucle externe s'exécutera pendant des
n+1
temps, le temps «1» signifie le processus qui vérifie si i répond toujours à l'exigence. Et la boucle intérieure s'exécuten
fois,n-2
fois .... Ainsi,0+2+..+(n-2)+n= (0+n)(n+1)/2= O(n²)
.Pour le code B, bien que la boucle interne n'intervienne pas et n'exécute pas foo (), la boucle interne sera exécutée pendant n fois en fonction du temps d'exécution de la boucle externe, qui est O (n)
la source
Je voudrais expliquer le Big-O sous un aspect un peu différent.
Big-O consiste simplement à comparer la complexité des programmes, ce qui signifie à quelle vitesse croissent-ils lorsque les intrants augmentent et non pas le temps exact consacré à l'action.
À mon humble avis dans les formules big-O, il vaut mieux ne pas utiliser d'équations plus complexes (vous pouvez simplement vous en tenir à celles du graphique suivant.) Cependant, vous pouvez toujours utiliser d'autres formules plus précises (comme 3 ^ n, n ^ 3, .. .) mais plus que cela peut parfois être trompeur! Il vaut donc mieux rester aussi simple que possible.
Je voudrais souligner une fois de plus qu'ici nous ne voulons pas obtenir une formule exacte pour notre algorithme. Nous voulons seulement montrer comment il croît lorsque les entrées augmentent et comparer avec les autres algorithmes dans ce sens. Sinon, vous feriez mieux d'utiliser différentes méthodes comme le benchmarking.
la source