Exemples pratiques en Java de la notation Big O

1. Vue d’ensemble

Dans ce tutoriel, nous allons parler de ce que signifie Big O Notation. Nous allons passer en revue quelques exemples pour étudier son effet sur la durée d’exécution de votre code.

2. L’intuition de la notation Big O

Nous entendons souvent la performance d’un algorithme décrit à l’aide de la notation Big O .

L’étude de la performance des algorithmes - ou complexité algorithmique - relève du domaine de l’analyse https://en.wikipedia.org/wiki/Analysis of algorithms[]. L’analyse d’algorithme répond à la question de savoir combien de ressources, telles que l’espace disque ou le temps, consomme un algorithme.

Nous regarderons le temps comme une ressource. En règle générale, moins il faut de temps pour un algorithme, mieux c’est.

3. Algorithmes à temps constant - O (1)

Comment cette taille d’entrée d’un algorithme affecte-t-elle son temps d’exécution? La clé pour comprendre Big O consiste à comprendre les vitesses auxquelles les choses peuvent évoluer Le taux en question ici est le temps pris par taille d’entrée

Considérons ce simple morceau de code:

int n = 1000;
System.out.println("Hey - your input is: " + n);

Clairement, peu importe ce que n est, ci-dessus. Ce morceau de code prend un temps constant pour s’exécuter. Cela ne dépend pas de la taille de n.

De même:

int n = 1000;
System.out.println("Hey - your input is: " + n);
System.out.println("Hmm.. I'm doing more stuff with: " + n);
System.out.println("And more: " + n);

L’exemple ci-dessus est également le temps constant. Même si cela prend 3 fois plus de temps à s’exécuter, cela ne dépend pas de la taille de l’entrée. Nous notons n. les algorithmes à temps constant comme suit: O (1) . Notez que O (2) , O (3) ou même O (1000) __ voudrait dire la même chose.

Nous ne nous soucions pas de savoir exactement combien de temps cela prend, cela prend seulement un temps constant.

4. Algorithmes logarithmiques temporels - O (log n)

Les algorithmes à temps constant sont (asymptotiquement) les plus rapides. Le temps logarithmique est le suivant le plus rapide. Malheureusement, ils sont un peu plus difficiles à imaginer.

Un exemple courant d’algorithme temporel logarithmique est l’algorithme https://en.wikipedia.org/wiki/Binary search algorithm[binary search]. Pour voir comment implémenter la recherche binaire en Java, cliquez sur le lien suivant:/java-binary-search[cliquez ici.]

Ce qui est important ici, c’est que le temps d’exécution augmente proportionnellement au logarithme de l’entrée (dans ce cas, connectez-vous à la base 2):

for (int i = 1; i < n; i = i **  2){
    System.out.println("Hey - I'm busy looking at: " + i);
}

Si n est 8, le résultat sera le suivant:

Hey - I'm busy looking at: 1
Hey - I'm busy looking at: 2
Hey - I'm busy looking at: 4

Notre algorithme simple a exécuté log (8) = 3 fois.

5. Algorithmes de temps linéaires - O (n)

Après les algorithmes de temps logarithmiques, nous obtenons la classe suivante la plus rapide:

  • algorithmes de temps linéaire. **

Si nous disons que quelque chose croît linéairement, nous voulons dire qu’il croît directement proportionnellement à la taille de ses entrées.

Pensez à une simple boucle for:

for (int i = 0; i < n; i++) {
    System.out.println("Hey - I'm busy looking at: " + i);
}

Combien de fois cette boucle est-elle exécutée? n fois, bien sûr! Nous ne savons pas exactement combien de temps cela prendra - et nous ne nous en inquiétons pas.

  • Ce que nous savons, c’est que l’algorithme simple présenté ci-dessus croîtra linéairement avec la taille de son entrée. **

Nous préférerions un temps d’exécution de 0.1n à (1000n 1000) , mais les deux algorithmes sont toujours linéaires; ils grandissent tous les deux directement proportionnellement à la taille de leurs intrants.

Encore une fois, si l’algorithme était modifié comme suit:

for (int i = 0; i < n; i++) {
    System.out.println("Hey - I'm busy looking at: " + i);
    System.out.println("Hmm.. Let's have another look at: " + i);
    System.out.println("And another: " + i);
}

Le runtime serait toujours linéaire dans la taille de son entrée, n . Nous désignons les algorithmes linéaires comme suit: O (n) .

Comme pour les algorithmes à temps constant, nous ne nous soucions pas des spécificités du runtime. O (2n 1) est identique à O (n) , étant donné que la notation Big O concerne la croissance pour la taille des entrées.

6. N Log N Algorithmes de temps - O (n log n)

  • n log n est la classe d’algorithmes suivante. ** Le temps d’exécution augmente proportionnellement à n log n de l’entrée:

for (int i = 1; i <= n; i++){
    for(int j = 1; j < 8; j = j **  2) {
        System.out.println("Hey - I'm busy looking at: " + i + " and " + j);
    }
}

Par exemple, si n est 8, cet algorithme exécutera 8 log (8) = 8 3 = 24 fois. Que nous ayons ou non une inégalité stricte dans la boucle est sans importance pour une notation Big O.

7. Algorithmes de temps polynomiaux - O (n ^ p ^)

Nous avons ensuite des algorithmes de temps polynomiaux. Ces algorithmes sont encore plus lents que n log n algorithmes.

Le terme polynôme est un terme général qui contient des fonctions quadratiques (n ^ 2 ^) , cubiques (n ^ 3 ^) , quartic (n ^ 4 ^) , etc. Ce qui est important à savoir, c’est que O (n ^ 2 ^) est plus rapide que O (n ^ 3 ^) , ce qui est plus rapide que O (n ^ 4 ^) , etc. **

Jetons un coup d’œil à un exemple simple d’algorithme de temps quadratique:

for (int i = 1; i <= n; i++) {
    for(int j = 1; j <= n; j++) {
        System.out.println("Hey - I'm busy looking at: " + i + " and " + j);
    }
}

Cet algorithme s’exécutera 8 ^ 2 ^ = 64 fois. Notez que si nous imbriquions une autre boucle for, cela deviendrait un algorithme O (n ^ 3 ^) .

8. Algorithmes de temps exponentiels - _O ( k ^ n ^) _

Maintenant nous entrons dans un territoire dangereux; ces algorithmes se développent proportionnellement à un facteur exprimé par la taille de l’entrée.

Par exemple, les algorithmes O (2 ^ n ^) doublent à chaque entrée supplémentaire. Ainsi, si n = 2 , ces algorithmes seront exécutés quatre fois; si n = 3 , ils fonctionneront huit fois (un peu comme le contraire des algorithmes de temps logarithmiques).

  • O (3 ^ n ^) algorithmes triples à chaque entrée supplémentaire, O (k ^ n ^) les algorithmes seront k fois plus grands à chaque entrée supplémentaire.

Jetons un coup d’œil à un exemple simple d’algorithme de temps O (2 ^ n ^) :

for (int i = 1; i <= Math.pow(2, n); i++){
    System.out.println("Hey - I'm busy looking at: " + i);
}

Cet algorithme fonctionnera 2 ^ 8 ^ = 256 fois.

9. Algorithmes de temps factoriels - O (n!)

Dans la plupart des cas, c’est aussi grave que possible. Cette classe d’algorithmes a une durée d’exécution proportionnelle à la factorial de la taille d’entrée.

Un exemple classique consiste à résoudre le problème https://en.wikipedia.org/wiki/Travelling salesman problem[traveling vendeur]en utilisant une approche brutale pour le résoudre.

Une explication de la solution au problème du voyageur voyageur dépasse le cadre de cet article.

Regardons plutôt un simple algorithme O (n!) , Comme dans les sections précédentes:

for (int i = 1; i <= factorial(n); i++){
    System.out.println("Hey - I'm busy looking at: " + i);
}

factorial (n) calcule simplement n !. Si n est 8, cet algorithme lancera 8! = 40320 fois.

10. Fonctions asymptotiques

  • Big O est ce que l’on appelle une fonction asymptotique . ** Tout cela signifie, c’est qu’il s’agit de la performance d’un algorithme à la limite - c’est-à-dire - lorsqu’il reçoit de nombreuses informations.

Big O ne se soucie pas de la qualité de votre algorithme avec des entrées de petite taille. Il s’agit de grandes entrées (pensez à trier une liste de un million de numéros plutôt qu’une liste de 5 numéros).

Une autre chose à noter est qu’il existe d’autres fonctions asymptotiques. Big Θ (thêta) et Big Ω (oméga) décrivent également des algorithmes à la limite (rappelez-vous, la limite, cela signifie simplement pour des entrées énormes).

Pour comprendre les différences entre ces 3 fonctions importantes, nous devons d’abord savoir que Big O, Big Θ et Big Ω décrivent un jeu (c’est-à-dire un ensemble d’éléments).

Ici, les membres de nos ensembles sont des algorithmes eux-mêmes:

  • Big O décrit l’ensemble des algorithmes qui exécutent __no pire que

certaine vitesse (c’est une limite supérieure) ** Inversement, Big Ω décrit l’ensemble des algorithmes exécutant __no

mieux qu’une certaine vitesse (c’est une limite inférieure) ** Enfin, Big décrit l’ensemble des algorithmes exécutant at a

certaine vitesse (c’est comme l’égalité)

Les définitions que nous avons données ci-dessus ne sont pas mathématiquement exactes, mais elles faciliteront notre compréhension.

  • Habituellement, vous entendez des choses décrites avec Big O ** , mais connaître les Big et Big Ω ne fait pas de mal.

11. Conclusion

Dans cet article, nous avons discuté de la notation Big O et de la façon dont comprendre la complexité d’un algorithme peut affecter le temps d’exécution de votre code.

Une excellente visualisation des différentes classes de complexité peut être trouvée ici.

Comme d’habitude, les extraits de code de ce didacticiel sont disponibles à l’adresse https://github.com/eugenp/tutorials/tree/master/algorithms-miscially-2 de GitHub].