Big O, comment le calcules-tu/approximation ?

La plupart des personnes titulaires d'un diplôme en informatique sauront certainement ce que signifie Big O. Cela nous aide à mesurer à quel point un algorithme est réellement (in)efficace et si vous savez dans dans quelle catégorie vous êtes en essayant de résoudre des problèmes, vous pouvez déterminer s'il est encore possible d'extraire ce petit supplément de performances.1

Mais je suis curieux, comment vous calculez ou approximez la complexité de vos algorithmes ?

1 mais comme on dit, n'en faites pas trop,

Quelques exemples d'utilisation en code C.

Disons 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) car peu importe la taille du tableau, il faut toujours le même temps constant 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'il faudrait 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 la première fois et parcourir la boucle une fois parce que Big-O décrit la limite supérieure d'un algorithme (oméga est pour la limite inférieure et thêta est pour la limite étroite) .

Lorsque nous arrivons 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 en nous laissant n au carré.

Cela ne fait qu'effleurer 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 familiarisera au moins avec les bases.

请先 登录 后评论
Community

La notation Big O est utile car elle est facile à utiliser et masque les complications et les détails inutiles (pour une certaine définition d'inutile). Une bonne façon de travailler sur la complexité des algorithmes de division pour régner est la méthode de l'arbre. Disons que vous avez une version de tri rapide avec la procédure médiane, donc vous divisez le tableau en sous-tableaux parfaitement équilibrés à chaque fois.

Construisez maintenant 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-tableaux. 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'arbre contient (au plus) le tableau entier, donc le travail par niveau est O(n) (les tailles des sous-tableaux s'additionnent jusqu'à n, et puisque nous avons O(k) par niveau, nous pouvons l'additionner) . Il n'y a que des niveaux log(n) dans l'arbre depuis chaque fois que nous réduisons de moitié l'entrée.

Nous pouvons donc majorer la quantité de travail par O(n*log(n)).

Cependant, Big O cache certains détails que nous ne pouvons parfois pas ignorer. Envisagez de calculer la suite de Fibonacci avec

a=0;
b=1;
for (i = 0; i <n; i++) {
    tmp = b;
    b = a + b;
    a = tmp;
}

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 qu'il s'agit d'un algorithme O(n) sans broncher. Le raisonnement est que vous avez n itérations dans la boucle for et que 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 le simple stockage prendra de l'ordre de n octets. Effectuer une addition avec de grands entiers prendra O(n) quantité de travail. Ainsi, la quantité totale de travail effectuée dans cette procédure est

1 2 3 ... n = n(n-1)/2 = O(n^2)

Cet algorithme s'exécute donc en temps quadradique !

请先 登录 后评论
Adam

En gros, ce qui revient 90 % du temps, c'est simplement l'analyse des boucles. Avez-vous des boucles imbriquées simples, doubles, triples ? Ensuite, vous avez O(n), O(n^2), O(n^3) temps d'exécution.

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 quelque chose de plus difficile que de simplement regarder vos boucles (pour les déclarations , tandis que, aller à, etc...)

请先 登录 后评论
OysterD

Petit rappel : la notation big O est utilisée pour désigner la complexité asymptotique (c'est-à-dire lorsque la taille du problème atteint l'infini), et il cache une constante.

Cela signifie qu'entre un algorithme en O(n) et un en O(n2), le plus rapide n'est pas toujours le premier (bien qu'il existe toujours une valeur de n telle que pour problèmes de taille >n, le premier algorithme est le plus rapide).

Notez que la constante cachée dépend beaucoup de l'implémentation !

De plus, dans certains cas, le temps d'exécution n'est pas une fonction déterministe de la taille n de l'entrée. Prenons par exemple le tri par tri rapide : 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 :

  • Le 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 An Introduction to the Analysis of Algorithms par R. Sedgewick et P. Flajolet.

Comme vous le dites, premature optimisation is the root of all evil et (si possible) le profilage devraient toujours être utilisés lors de l'optimisation du code. Cela peut même vous aider à déterminer la complexité de vos algorithmes.

请先 登录 后评论