web-dev-qa-db-fra.com

Big-O pour huit ans?

Je demande plus sur ce que cela signifie pour mon code. Je comprends les concepts mathématiquement, j'ai juste du mal à comprendre ce qu’ils signifient sur le plan conceptuel. Par exemple, si vous exécutez une opération O(1) sur une structure de données, je comprends que le nombre d'opérations à effectuer ne va pas augmenter car il y a plus d'éléments. Et Une opération O(n)] signifierait que vous effectueriez un ensemble d'opérations sur chaque élément. Quelqu'un pourrait-il renseigner les blancs ici?

  • Comme quoi faire exactement une opération O (n ^ 2)?
  • Et qu'est-ce que ça veut dire si une opération est O (n log (n))?
  • Et est-ce que quelqu'un doit fumer du crack pour écrire un O (x!)?
301
Jason Baker

Une façon de penser à cela est la suivante:

O (N ^ 2) signifie que pour chaque élément, vous faites quelque chose avec tous les autres éléments, par exemple en les comparant. Le tri à bulles en est un exemple.

O (N log N) signifie que pour chaque élément, vous faites quelque chose qui n'a besoin que d'examiner le log N des éléments. C’est généralement parce que vous connaissez les éléments qui vous permettent de faire un choix efficace. Les tris les plus efficaces en sont un exemple, comme le tri par fusion.

O (N!) Signifie faire quelque chose pour toutes les permutations possibles des N éléments. Voyageur est un exemple de cela, où il y a N! façons de visiter les nœuds, et la solution en force brute consiste à examiner le coût total de toutes les permutations possibles afin de trouver la solution optimale.

290
Don Neufeld

La grosse chose que la notation Big-O signifie pour votre code est la façon dont il va évoluer lorsque vous doublez le nombre de "choses" sur lesquelles il opère. Voici un exemple concret:

 Big-O | calculs pour 10 choses | calculs pour 100 choses 
 --------------------------------------------- ----------------------------- 
 O (1) | 1 | 1 
 O (log (n)) | 3 | 7 
 O (n) | 10 | 100 
 O (n log (n)) | 30 | 700 
 O (n ^ 2) | 100 | 10000 

Alors prenez quicksort qui est O (n log (n)) vs type de bulle qui est O (n ^ 2). Lors du tri de 10 choses, le tri rapide est 3 fois plus rapide que le tri à bulles. Mais pour trier 100 choses, c'est 14 fois plus vite! Il est donc important de choisir l’algorithme le plus rapide. Lorsque vous atteignez des bases de données contenant plusieurs millions de lignes, cela peut faire toute la différence entre une requête exécutée en 0,2 seconde et une heure de travail.

Une autre chose à considérer est qu'un mauvais algorithme est une chose que la loi de Moore ne peut pas aider. Par exemple, si vous avez un calcul scientifique qui est O (n ^ 3) et qu'il peut calculer 100 choses par jour, doubler la vitesse du processeur ne vous rapporte que 125 choses par jour. Cependant, mettez ce calcul à O (n ^ 2) et vous ferez 1000 choses par jour.

clarification: En fait, Big-O ne dit rien sur les performances comparatives de différents algorithmes au même point de taille spécifique, mais plutôt sur les performances comparatives du même algorithme à des points de taille différents:

                 calculs calculs calculs 
 Big-O | pour 10 choses | pour 100 choses | pour 1000 choses 
 ---------------------------------------------- ---------------------------- 
 O (1) | 1 | 1 | 1 
 O (log (n)) | 1 | 3 | 7 
 O (n) | 1 | 10 | 100 
 O (n log (n)) | 1 | 33 | 664 
 O (n ^ 2) | 1 | 100 | 10000 
263
bmdhacks

Vous trouverez peut-être utile de le visualiser:

Big O Analysis

En outre, sur LogY/LogX , mettez les fonctions à l'échelle n1/2, n, n2 tous ressemblent à lignes droites , tandis que sur échelle LogY/X  2nen, dixn sont des lignes droites et n ! est linearithmic (on dirait n log n).

110
Matthew Rapati

C'est peut-être trop mathématique, mais voici mon essai. (Je suis un mathématicien.)

Si quelque chose est O ( f ( n )), alors il est en cours d'exécution le temps sur n éléments sera égal à [~ # ~] a [~ # ~] f ( n ) + [~ # ~] b [~ # ~] (mesuré, par exemple, en cycles d'horloge ou en opérations de la CPU). Il est essentiel de comprendre que vous avez également ces constantes [~ # ~] a [~ # ~] et [ ~ # ~] b [~ # ~] , résultant de la mise en oeuvre spécifique. [~ # ~] b [~ # ~] représente essentiellement le "temps système constant" de votre opération, par exemple un prétraitement que vous faites qui ne le fait pas. ne dépend pas de la taille de la collection. [~ # ~] a [~ # ~] représente la vitesse de votre algorithme de traitement d'élément.

La clé, cependant, est que vous utilisez la grande notation O pour déterminer à quel point quelque chose va évoluer. Ces constantes n’auront donc aucune importance: si vous essayez de comprendre comment passer de 10 à 10000 articles, qui se soucie des frais généraux constants [~ # ~] b [~ # ~] ? De même, d'autres préoccupations (voir ci-dessous) l'emporteront certainement sur le poids de la constante multiplicative [~ # ~] a [~ # ~] .

Donc, la vraie affaire est f ( n ). Si f ne croît pas du tout avec n , par ex. f ( n ) = 1, alors vous évoluez de manière fantastique- --votre durée sera toujours juste [~ # ~] a [~ # ~] + [~ # ~] b [~ # ~] . Si f croît linéairement avec n , c'est-à-dire f ( n ) = n , votre temps d’exécution s’échelonnera du mieux que vous pouvez l’espérer --- si vos utilisateurs attendent 10 ns pour 10 éléments, ils attendront 10 000 ns pour 10 000 éléments (en ignorant la constante additive). Mais si ça pousse plus vite, comme n 2, alors vous êtes en difficulté; les choses commenceront à ralentir beaucoup trop quand vous aurez de plus grandes collections. f ( n ) = n journal ( n ) est un bon compromis, généralement: votre opération ne peut pas être aussi simple que de donner mise à l'échelle linéaire, mais vous avez réussi à réduire les choses de telle sorte que l'échelle sera bien meilleure que f ( n ) = n 2.

En pratique, voici quelques bons exemples:

  • O (1): récupérer un élément d'un tableau. Nous savons exactement où il se trouve dans la mémoire, alors nous allons simplement le chercher. Peu importe si la collection contient 10 articles ou 10 000; il est toujours à l'index (disons) 3, alors nous sautons simplement à l'emplacement 3 en mémoire.
  • O ( n ): récupération d'un élément à partir d'une liste chaînée. Ici, [~ # ~] un [~ # ~] = 0,5, car en moyenne, vous devrez passer par la moitié de la liste chaînée avant de trouver l'élément que vous recherchez.
  • O(n2): divers algorithmes de tri "muets". Parce que généralement leur stratégie implique, pour chaque élément ( n ), vous regardez tous les autres éléments (donc parfois un autre n , donnant n 2), puis positionnez-vous au bon endroit.
  • O ( n log ( n )): divers classements "intelligents" algorithmes. Il s’avère qu’il suffit de regarder, par exemple, 10 éléments sur 10dix-element collection pour vous trier intelligemment par rapport à tout le monde dans la collection. Parce que tout le monde est aussi va examiner 10 éléments, et le comportement émergent est orchestré juste comme il convient pour que cela soit suffisant pour produire une liste triée.
  • O ( n !): Un algorithme qui "essaie tout", puisqu'il existe (proportionnellement à) n ! combinaisons possibles de n éléments susceptibles de résoudre un problème donné. Donc, il passe en revue toutes ces combinaisons, les essaie, puis s’arrête dès qu’il réussit.
71
Domenic

la réponse de don.neufeld est très bonne, mais je l'expliquerais probablement en deux parties: premièrement, il existe une hiérarchie approximative de O () dans laquelle la plupart des algorithmes se situent. Ensuite, vous pouvez examiner chacun de ces éléments pour élaborer des esquisses de ce que font les algorithmes typiques de cette complexité temporelle.

Pour des raisons pratiques, les seuls O () qui semblent avoir toujours de l'importance sont les suivants:

  • O (1) "temps constant" - le temps requis est indépendant de la taille de l'entrée. En tant que catégorie approximative, j'inclurais ici des algorithmes tels que les recherches de hachage et Union-Find, même si aucun de ceux-ci n'est réellement O (1).
  • O (log (n)) "logarithmique" - il ralentit à mesure que vous obtenez des entrées plus volumineuses, mais une fois que votre entrée devient assez volumineuse, elle ne changera pas suffisamment pour vous inquiéter Si votre exécution fonctionne correctement avec des données de taille raisonnable, vous pouvez la submerger avec autant de données supplémentaires que vous le souhaitez et tout ira bien.
  • O (n) "linéaire" - plus le nombre d'entrées est long, plus il faut, dans un compromis égal. Trois fois la taille de l'entrée prendra environ trois fois plus de temps.
  • O (n log (n)) "meilleur que quadratique" - augmenter la taille de l'entrée fait mal, mais cela reste gérable. L'algorithme est probablement correct, c'est simplement que le problème sous-jacent est plus difficile (les décisions sont moins localisées par rapport aux données d'entrée) que les problèmes pouvant être résolus en temps linéaire. Si la taille de vos entrées augmente, ne présumez pas que vous pouvez nécessairement gérer deux fois la taille sans modifier votre architecture (par exemple en déplaçant des éléments dans des calculs par lots de nuit ou en ne faisant pas les choses par image). Ce n'est pas grave si la taille d'entrée augmente un peu, cependant; faites juste attention aux multiples.
  • O (n ^ 2) "quadratique" - cela ne fonctionnera vraiment que jusqu'à une certaine taille de votre entrée, alors faites attention à la taille qu'elle pourrait avoir. En outre, votre algorithme peut être nul - pensez bien s'il existe un algorithme O (n log (n)) qui vous donnerait ce dont vous avez besoin. Une fois que vous êtes ici, soyez très reconnaissant pour le matériel incroyable qui nous a été offert. Il n'y a pas si longtemps, ce que vous tentiez de faire aurait été impossible à toutes fins pratiques.
  • O(n^3) "cubic" - not qualitatively all that different from O(n^2). The same comments apply, only more so. There's a decent chance that a more clever algorithm could shave this time down to something smaller, eg O(n^2 log(n)) or O(n^2.8...), but then again, there's a good chance that it won't be worth the trouble. (You're already limited in your practical input size, so the constant factors that may be required for the more clever algorithms will probably swamp their advantages for practical cases. Also, thinking is slow; letting the computer chew on it may save you time overall.)
  • O (2 ^ n) "exponentiel" - le problème est soit difficile à calculer, soit vous êtes un idiot. Ces problèmes ont une saveur reconnaissable. Vos tailles d’entrée sont limitées à une limite stricte assez spécifique. Vous saurez rapidement si vous vous situez dans cette limite.

Et c'est tout. Il existe de nombreuses autres possibilités qui correspondent à celles-ci (ou sont supérieures à O (2 ^ n)), mais elles ne se produisent pas souvent dans la pratique et ne sont pas qualitativement très différentes de l'une d'elles. Les algorithmes cubiques sont déjà un peu exagérés; Je ne les ai inclus que parce que je les ai rencontrés assez souvent pour être dignes de mention (par exemple, la multiplication matricielle).

Que se passe-t-il réellement pour ces classes d'algorithmes? Eh bien, je pense que vous avez eu un bon départ, bien que de nombreux exemples ne correspondent pas à ces caractérisations. Mais pour ce qui précède, je dirais que ça va généralement quelque chose comme:

  • O (1) - vous ne regardez au maximum qu’un bloc de taille fixe de vos données d’entrée, voire aucune. Exemple: le maximum d’une liste triée.
    • Ou votre taille d'entrée est délimitée. Exemple: addition de deux nombres. (Notez que l'ajout de N nombres est un temps linéaire.)
  • O (log n) - chaque élément de votre entrée vous en dit assez pour ignorer une grande partie du reste de l'entrée. Exemple: lorsque vous regardez un élément de tableau dans une recherche binaire, sa valeur vous indique que vous pouvez ignorer "la moitié" de votre tableau sans le regarder. De même, l'élément que vous regardez vous donne suffisamment de résumé d'une fraction de l'entrée restante pour que vous n'ayez pas besoin de l'examiner.
    • Il n'y a cependant rien de spécial dans les moitiés: si vous ne pouvez ignorer que 10% de vos contributions à chaque étape, cela reste logarithmique.
  • O(n) - you do some fixed amount of work per input element. (But see below.)
  • O (n log (n)) - il existe quelques variantes.
    • Vous pouvez diviser l’entrée en deux piles (pas plus que temps linéaire), résoudre le problème indépendamment sur chaque pile, puis combiner les deux piles pour former la solution finale. L'indépendance des deux piles est la clé. Exemple: mergesort classique récursif.
    • Chaque passage linéaire sur les données vous amène à mi-chemin de votre solution. Exemple: quicksort si vous pensez en termes de distance maximale de chaque élément à sa position finale triée à chaque étape de partitionnement (et oui, je sais qu’il s’agit en fait de O (n ^ 2) à cause de choix de pivots dégénérés. tombe dans ma catégorie O (n log (n)).)
  • O (n ^ 2) - vous devez regarder chaque paire d’éléments d’entrée.
    • Ou vous ne le faites pas, mais vous pensez le faire et vous utilisez le mauvais algorithme.
  • O (n ^ 3) - euh ... Je n'ai pas une caractérisation rapide de ceux-ci. C'est probablement l'un des:
    • Vous multipliez les matrices
    • Vous examinez chaque paire d'entrées, mais l'opération que vous effectuez nécessite de réexaminer toutes les entrées.
    • toute la structure graphique de votre entrée est pertinente
  • O (2 ^ n) - vous devez prendre en compte tous les sous-ensembles possibles de vos entrées.

Aucune d'entre elles n'est rigoureuse. Algorithmes de temps non spécialement linéaires (O (n)): Je pourrais vous donner un certain nombre d’exemples dans lesquels vous devez examiner toutes les entrées, puis la moitié, puis la moitié, etc. Ou l’inverse - - vous pliez des paires d’entrées, puis recurse sur la sortie. Celles-ci ne correspondent pas à la description ci-dessus, car vous ne regardez pas chaque entrée, mais elle apparaît toujours en temps linéaire. Pourtant, 99,2% du temps, le temps linéaire consiste à examiner chaque entrée une fois.

59
sfink

Beaucoup d'entre elles sont faciles à démontrer avec quelque chose de non programmé, comme le brassage de cartes.

Trier un jeu de cartes en parcourant tout le jeu pour trouver l'as de pique, puis en parcourant tout le jeu pour trouver le 2 de pique, et ainsi de suite, serait le pire des cas, si le jeu était déjà trié à l'envers. Vous avez regardé les 52 cartes 52 fois.

En général, les très mauvais algorithmes ne sont pas nécessairement intentionnels, ils constituent généralement un abus de quelque chose d'autre, comme appeler une méthode linéaire dans une autre méthode qui répète de manière linéaire le même ensemble.

21
John Gardner

D'accord, il y a de très bonnes réponses ici, mais presque toutes semblent faire la même erreur et c'est une erreur qui imprègne l'usage commun.

De manière informelle, nous écrivons que f(n) = O (g(n)) si, jusqu'à un facteur de mise à l'échelle et pour tout n supérieur à certains n0, g(n) est plus grand que f (n). C'est-à-dire que f(n) ne grandit pas plus vite que, ou est borné par le haut par, g (n). Cela ne nous dit rien sur la rapidité avec laquelle f(n) croît, à l'exception du fait qu'il est garanti qu'elle ne sera pas pire que g (n).

Un exemple concret: n = O (2 ^ n). Nous savons tous que n grandit beaucoup moins vite que 2 ^ n, de sorte que nous pouvons dire qu’il est limité par la fonction exponentielle. Il y a beaucoup de place entre n et 2 ^ n, donc ce n'est pas très lié , mais c'est toujours un lien légitime.

Pourquoi nous (informaticiens) utilisons-nous des limites plutôt que d'être exactes? Parce que a) les bornes sont souvent plus faciles à prouver et b) cela nous donne un raccourci pour exprimer les propriétés des algorithmes. Si je dis que mon nouvel algorithme est O (n.log n), cela signifie que, dans le pire des cas, son temps d'exécution sera limité d'en haut par n.log n sur n entrées, pour assez grand n (bien que voir mes commentaires ci-dessous quand je ne veux pas dire le pire des cas).

Si au lieu de cela, nous voulons dire qu'une fonction croît exactement aussi vite qu'une autre, nous utilisons theta pour préciser ce point (j'écrirai T (f(n)) signifie\Theta de f(n) dans le démarque). T (g(n)) est un raccourci pour être borné de au-dessus et au-dessous par g (n), encore une fois, jusqu'à un facteur de mise à l'échelle et de manière asymptotique.

C'est-à-dire f(n) = T (g(n)) <=> f(n) = O(g(n)) et g(n) = O (f (n)). Dans notre exemple, nous pouvons voir que n! = T (2 ^ n) parce que 2 ^ n! = O (n).

Pourquoi s'inquiéter à ce sujet? Parce que dans votre question, vous écrivez "Quelqu'un devrait-il fumer du crack pour écrire un O (x!)?" La réponse est non, car fondamentalement tout ce que vous écrivez sera limité d'en haut par la fonction factorielle. Le temps d'exécution du tri rapide est O (n!) - ce n'est tout simplement pas serré.

Il y a aussi une autre dimension de subtilité ici. En règle générale, nous parlons de la entrée dans le pire des cas lorsque nous utilisons la notation O (g(n)), de sorte que nous faisons une instruction composée: dans le pire des cas, exécuter Dans le temps, ce ne sera pas pire qu’un algorithme qui prend g(n), pas à nouveau, modulo scaling et n suffisant. Mais parfois, nous voulons parler de la durée d'exécution de la moyenne et même de la meilleure cas.

Vanilla Quicksort est, comme toujours, un bon exemple. Il s’agit de T (n ^ 2) dans le pire des cas (il faudra en fait au moins n ^ 2 étapes, mais pas beaucoup plus), mais T (n.log n) dans le cas moyen, c’est-à-dire le nombre attendu de steps est proportionnel à n.log n. Dans le meilleur des cas, il s’agit également de T (n.log n) - mais vous pouvez l’améliorer, par exemple, en vérifiant si le tableau a déjà été trié, auquel cas la meilleure durée d’exécution sera T (n).

Comment cela se rapporte-t-il à votre question sur les réalisations pratiques de ces limites? Malheureusement, la notation O () cache des constantes auxquelles les implémentations du monde réel doivent faire face. Ainsi, bien que nous puissions dire que, par exemple, pour une opération T (n ^ 2), nous devons visiter tous les éléments possibles, nous ne savons pas combien de fois nous devons les visiter (sauf que ce n'est pas une fonction de n). Donc, nous pourrions avoir à visiter chaque paire 10 fois, ou 10 ^ 10 fois, et la déclaration T (n ^ 2) ne fait aucune distinction. Les fonctions d'ordre inférieur sont également cachées - nous pourrions devoir visiter chaque paire d'éléments une fois et chaque élément 100 fois, car n ^ 2 + 100n = T (n ^ 2). L'idée derrière la notation O () est que pour n assez grand, cela n'a pas d'importance, car n ^ 2 est tellement plus grand que 100n que nous ne remarquons même pas l'impact de 100n sur le temps d'exécution. Cependant, nous traitons souvent avec n suffisamment petit pour que des facteurs constants et ainsi de suite fassent une différence réelle et significative.

Par exemple, quicksort (coût moyen T (n.log n)) et heapsort (coût moyen T (n.log n)) sont tous deux des algorithmes de tri ayant le même coût moyen, mais quicksort est généralement beaucoup plus rapide que heapsort. En effet, Heapsort effectue un peu plus de comparaisons par élément que quicksort.

Cela ne veut pas dire que la notation O () est inutile, mais simplement imprécise. C'est un outil assez grossier à manier pour les petits n.

(En guise de conclusion au présent traité, rappelez-vous que la notation O () décrit simplement la croissance de toute fonction - cela ne doit pas nécessairement être du temps, cela peut être de la mémoire, des messages échangés dans un système distribué ou le nombre de CPU requis pour la sauvegarde. un algorithme parallèle.)

18
HenryR

J'essaie d'expliquer en donnant des exemples de code simples en C #.

Pour List<int> numbers = new List<int> {1,2,3,4,5,6,7,12,543,7};

O (1) ressemble à

return numbers.First();

O (n) ressemble à

int result = 0;
foreach (int num in numbers)
{
    result += num;
}
return result;

O (n log (n)) ressemble à

int result = 0;
foreach (int num in numbers)
{
    int index = numbers.length - 1;
    while (index > 1)
    {
        // yeah, stupid, but couldn't come up with something more useful :-(
        result += numbers[index];
        index /= 2;
    }
}
return result;

O (n ^ 2) ressemble à

int result = 0;
foreach (int outerNum in numbers)
{
    foreach (int innerNum in numbers)
    {
        result += outerNum * innerNum;
    }
}
return result;

O (n!) Ressemble, euh, trop fatigué pour trouver quelque chose de simple.
Mais j'espère que vous comprenez le problème général?

18
Albin Sunnanbo

Voici comment je le décris à mes amis non techniques:

Envisagez une addition à plusieurs chiffres. Bon ajout démodé, crayon et papier. Le genre que tu as appris quand tu avais 7-8 ans. Avec deux nombres à trois ou quatre chiffres, vous pouvez savoir à quoi ils correspondent assez facilement.

Si je vous donnais deux nombres de 100 chiffres et que je vous demandais à quoi ils correspondent, il serait assez simple de le comprendre, même si vous deviez utiliser un crayon et du papier. Un enfant brillant pourrait faire un tel ajout en quelques minutes. Cela ne nécessiterait qu'environ 100 opérations.

Maintenant, considérons la multiplication à plusieurs chiffres. Vous l'avez probablement appris vers 8 ou 9 ans. Vous avez (espérons-le) fait beaucoup d'exercices répétitifs pour apprendre les mécanismes derrière tout cela.

Maintenant, imaginez que je vous ai donné ces deux mêmes nombres à 100 chiffres et que je vous ai dit de les multiplier ensemble. Ce serait une tâche beaucoup plus difficile beaucoup , quelque chose qui vous prendrait des heures - et qu'il serait peu probable que vous fassiez sans erreurs. La raison en est que (cette version de) la multiplication est O (n ^ 2); chaque chiffre du nombre du bas doit être multiplié par chaque chiffre du nombre du haut, pour un total d'environ n ^ 2 opérations. Dans le cas des nombres à 100 chiffres, cela correspond à 10 000 multiplications.

12
Aric TenEyck

Je pense que vous avez pour tâche de résoudre un problème causé par un méchant méchant V qui choisit N, et vous devez estimer combien de temps il faudra pour que le problème soit réglé lorsqu'il augmentera N.

O (1) -> augmenter N ne fait vraiment aucune différence

O (log (N)) -> chaque fois que V double N, vous devez passer plus de temps T pour terminer la tâche. V double encore N et vous dépensez le même montant.

O (N) -> chaque fois que V double N, vous passez deux fois plus de temps.

O(N^2) -> every time V doubles N, you spend 4x as much time. (it's not fair!!!)

O (N log (N)) -> chaque fois que V double N, vous passez deux fois plus de temps et un peu plus.

Ce sont les limites d'un algorithme; Les informaticiens veulent décrire le temps que cela prendra pour les grandes valeurs de N. (ce qui devient important lorsque vous factorisez des nombres utilisés en cryptographie - si les ordinateurs sont multipliés par 10, combien de bits de plus vous devez utiliser pour vous assurer qu'il leur faudra encore 100 ans pour casser votre cryptage et pas seulement un an?)

Certaines limites peuvent avoir des expressions étranges si cela fait une différence pour les personnes impliquées. J'ai vu des choses comme O (N log (N) log (log (log))) quelque part dans l'art de la programmation informatique de Knuth pour certains algorithmes. (Je ne me souviens plus lequel de mes souvenirs)

5
Jason S

Non, un algorithme O(n)) ne signifie pas qu’il effectuera une opération sur chaque élément. La notation Big-O vous permet de parler de la "vitesse" de votre algorithme, indépendamment de votre machine réelle.

O (n) signifie que le temps que prendra votre algorithme augmente linéairement à mesure que votre entrée augmente. O (n ^ 2) signifie que le temps que prend votre algorithme augmente comme le carré de votre entrée. Et ainsi de suite.

5
Esteban Araya

Une chose qui n'a pas encore été évoquée pour une raison quelconque:

Quand vous voyez des algorithmes avec des choses comme O (2 ^ n) ou O (n ^ 3) ou d'autres valeurs désagréables, cela signifie souvent que vous allez devoir accepter une réponse imparfaite à votre problème pour obtenir des performances acceptables.

Les solutions correctes qui explosent de la sorte sont courantes lorsqu'il s'agit de problèmes d'optimisation. Une réponse presque correcte fournie dans un délai raisonnable est préférable à une réponse correcte fournie longtemps après que la machine se soit décomposée en poussière.

Pensez aux échecs: je ne sais pas exactement quelle solution est considérée comme correcte, mais c'est probablement quelque chose comme O (n ^ 50) ou même pire. Il est théoriquement impossible à un ordinateur de calculer la bonne réponse - même si vous utilisez chaque particule de l'univers comme un élément informatique effectuant une opération dans le temps le plus court possible pour la vie de l'univers, il vous reste encore beaucoup de zéros . (La question de savoir si un ordinateur quantique peut résoudre le problème est une autre affaire.)

4
Loren Pechtel
  • Et est-ce que quelqu'un doit fumer du crack pour écrire un O (x!)?

Non, utilisez simplement Prolog. Si vous écrivez un algorithme de tri dans Prolog en décrivant simplement que chaque élément doit être plus grand que le précédent et laissez le retour en arrière faire le tri pour vous, ce sera O (x!). Aussi connu sous le nom de "tri par permutation".

3
Anders Öhrt

L'intuitition derrière Big-O

Imaginez une "compétition" entre deux fonctions sur x, lorsque x approche de l'infini: f(x) et g (x).

Maintenant, si à un moment donné (une x), une fonction a toujours une valeur supérieure à l’autre, appelons cette fonction "plus vite" que l’autre.

Ainsi, par exemple, si pour chaque x> 100, vous voyez que f(x)> g (x), alors f(x) est "plus rapide" "que g (x).

Dans ce cas, on dirait g(x) = O (f (x)). f(x) pose une sorte de "limite de vitesse" de trie pour g (x), puisqu’il passe finalement et le laisse pour de bon.

Ce n'est pas exactement la définition de notation big-O , ce qui indique également que f(x) doit seulement être plus grand que C * g (x) pour une constante C (qui est juste une autre façon de dire que vous ne pouvez pas aider g(x)) gagner le concours en le multipliant par un facteur constant - f(x) finira toujours par gagner). La définition formelle utilise également des valeurs absolues, mais j’espère avoir réussi à la rendre intuitive.

3
Assaf Lavie

J'aime la réponse de don neufeld, mais je pense pouvoir ajouter quelque chose à propos de O (n log n).

Un algorithme utilisant une simple stratégie de division et de conquête sera probablement O (log n). L'exemple le plus simple consiste à trouver quelque chose dans une liste triée. Vous ne commencez pas au début et ne le recherchez pas. Vous vous rendez au milieu, vous décidez si vous devez alors revenir en arrière ou en avant, sauter à mi-chemin du dernier endroit que vous avez cherché et répétez cette opération jusqu'à ce que vous trouviez l'élément que vous recherchez.

Si vous examinez les algorithmes quicksort ou mergesort, vous verrez qu'ils adoptent tous deux l'approche consistant à diviser la liste pour la trier en deux, en triant chaque moitié (en utilisant le même algorithme, de manière récursive), puis en recombinant les deux moitiés. Ce type de stratégie de division récursive sera O (n log n).

Si vous y réfléchissez bien, vous verrez que quicksort applique un algorithme de partitionnement O(n)) sur les n éléments complets, puis un O(n) = partitionner deux fois sur n/2 éléments, puis 4 fois sur n/4 éléments, etc ... jusqu'à ce que vous obteniez n partitions sur 1 élément (ce qui est dégénéré). Le nombre de fois que vous divisez n en deux pour arriver à 1 correspond approximativement à log n et chaque étape correspond à O (n), de sorte que la division et la conquête récursives sont valables O (n log n). , où la recombinaison de deux listes triées est O (n).

Quant à fumer du crack pour écrire un algorithme O (n!), Vous l’êtes à moins que vous n’ayez pas le choix. On pense que le problème de vendeur voyageur mentionné ci-dessus est l'un de ces problèmes.

2
archbishop

La plupart des livres de Jon Bentley (par exemple Programming Pearls ) traitent de tels sujets de manière très pragmatique. Cette conversation donnée par lui inclut une telle analyse d'un tri rapide.

Bien que cela ne soit pas tout à fait pertinent pour la question, Knuth a proposé une idée intéressante : enseigner la notation Big-O dans les classes de calcul au lycée, bien que je trouve cette idée plutôt excentrique.

2
user9282

Pensez-y comme si vous empiliez des lego (n) qui se superposaient verticalement et sautaient par dessus.

O (1) signifie à chaque étape, vous ne faites rien. La hauteur reste la même.

O (n) signifie qu'à chaque étape, vous empilez c blocs, où c1 est une constante.

O (n ^ 2) signifie qu'à chaque étape, vous empilez c2 x n blocs, où c2 est une constante et n est le nombre de blocs empilés.

O (nlogn) signifie qu'à chaque étape, vous empilez c3 x n x log n blocs, où c3 est une constante et n est le nombre de blocs empilés.

2
yogman

Vous souvenez-vous de la fable de la tortue et du lièvre (tortue et lapin)?

À long terme, la tortue gagne, mais à court terme, le lièvre gagne.

C'est comme O(logN) (tortue) vs O(N) (lièvre).

Si deux méthodes diffèrent par leur grand-O, il y a un niveau de N auquel l'une d'entre elles va gagner, mais big-O ne dit rien sur la taille de ce N.

1
Mike Dunlavey

Supposons que vous disposiez d'un ordinateur capable de résoudre un problème d'une certaine taille. Maintenant, imaginons que nous pouvons doubler la performance à quelques reprises. Combien de problèmes pouvons-nous résoudre avec chaque doublement?

Si nous pouvons résoudre un problème de taille double, c'est O (n).

Si nous avons un multiplicateur qui n'en est pas un, c'est une sorte de complexité polynomiale. Par exemple, si chaque doublement nous permet d’augmenter la taille du problème d’environ 40%, c’est O (n ^ 2), et environ 30% serait O (n ^ 3).

Si nous ajoutons simplement à la taille du problème, c'est exponentiel ou pire. Par exemple, si chaque doublement signifie que nous pouvons résoudre un problème 1 plus gros, c’est O (2 ^ n). (C’est pourquoi il est impossible de forcer brutalement une clé de chiffrement avec des clés de taille raisonnable: une clé de 128 bits nécessite environ 16 milliards de fois le traitement nécessaire d’une clé 64 bits.)

1
David Thornley

Dites à votre log âgé de huit ans (n) signifie le nombre de fois que vous devez couper une longueur n log en deux pour que sa taille descende à n = 1: p

O (n log n) est habituellement le tri O (n ^ 2) est habituellement la comparaison de toutes les paires d'éléments

1
Chad Brewbaker

Juste pour répondre aux quelques commentaires sur mon post ci-dessus:

Domenic - Je suis sur ce site et je m'en soucie. Pas pour le pédantisme, mais parce que nous, les programmeurs, tenons généralement à la précision. Utiliser la notation O () de manière incorrecte dans le style que certains ont fait ici la rend insignifiante; nous pouvons aussi bien dire que quelque chose prend n ^ 2 unités de temps comme O (n ^ 2) selon les conventions utilisées ici. L'utilisation du O () n'ajoute rien. Ce dont je parle, ce n’est pas seulement un petit écart entre l’usage courant et la précision mathématique, c’est la différence entre le sens et le non-sens.

Je connais beaucoup d'excellents programmeurs qui utilisent ces termes avec précision. Dire "oh, nous sommes des programmeurs, donc on s'en fiche" déprécie l'entreprise.

onebyone - Eh bien, pas vraiment bien que je prenne votre point. Ce n'est pas O(1) pour n arbitrairement grand, ce qui est un peu la définition de O (). Cela montre simplement que O () a une applicabilité limitée pour n borné, où plutôt parler du nombre de pas plutôt que d’une limite sur ce nombre.

1
HenryR

Pour comprendre O (n log n), rappelez-vous que log n signifie log-base-2 of n. Ensuite, regardez chaque partie:

O (n) est, plus ou moins, lorsque vous opérez sur chaque élément de l'ensemble.

O (log n) est quand le nombre d'opérations est le même que l'exposant auquel vous soulevez 2, pour obtenir le nombre d'items. Une recherche binaire, par exemple, doit couper l'ensemble de moitié log n fois.

O (n log n) est une combinaison - vous effectuez quelque chose dans les lignes d'une recherche binaire pour chaque élément de l'ensemble. Les tris efficaces fonctionnent souvent en effectuant une boucle par élément et en effectuant une bonne recherche pour trouver le bon endroit pour placer l'élément ou le groupe en question. D'où n * log n.

1
Kevin Conner

Pour rester sincère face à la question posée, je répondrais à la question de la même manière que je le ferais pour un enfant de 8 ans.

Supposons qu'un marchand de glaces prépare un certain nombre de glaces (disons N) de différentes formes, disposées de manière ordonnée. Vous voulez manger la glace se trouvant au milieu

Cas 1: - Vous ne pouvez manger une glace que si vous avez mangé toutes les glaces plus petites que cela. Vous devrez manger la moitié de toutes les glaces préparées (entrée). La réponse dépend directement de la taille de l'entrée La solution sera d'ordre o (N)

Cas 2: - Vous pouvez directement manger la glace au milieu

La solution sera O (1)

Cas 3: Vous ne pouvez manger une glace que si vous avez mangé toutes les glaces plus petites et chaque fois que vous mangez une glace, vous permettez à un autre enfant (un enfant neuf à chaque fois) de manger toutes ses glaces. Le temps total pris serait de N + N + N ....... (N/2) fois la solution sera O (N2)

1
user3347123

Je vais essayer d'écrire une explication pour un vrai garçon de huit ans, en plus des termes techniques et des notions mathématiques.

Par exemple, que ferait exactement une opération O(n^2)?

Si vous êtes dans une fête et qu'il y a n personnes dans la fête, y compris vous. Combien de poignées de main faut-il pour que tout le monde ait une poignée de main, sachant que les gens oublieront probablement qui ils ont une poignée de main à un moment donné.

Remarque: cela ressemble à un simplexe donnant n(n-1) qui est assez proche de n^2.

Et qu'est-ce que ça veut dire si une opération est O(n log(n))?

Votre équipe favorite a gagné, elle fait la queue et il y a n joueurs. Combien de hanshakes cela vous prendrait pour serrer la main de chaque joueur, étant donné que vous hanshake chacun plusieurs fois, combien de fois, combien de chiffres sont dans le nombre de joueurs n.

Remarque: cela donnera n * log n to the base 10.

Et est-ce que quelqu'un doit fumer du crack pour écrire une O(x!)?

Vous êtes un enfant riche et dans votre armoire il y a beaucoup de vêtements, il y a x tiroirs pour chaque type de vêtement, les tiroirs sont juxtaposés, le premier tiroir a 1 article, chaque tiroir a autant comme dans le tiroir à sa gauche et un de plus, vous avez donc quelque chose comme: chapeau 1, perruques 2, .. (x-1) pantalons, puis x chemises. Maintenant, de combien de façons pouvez-vous vous habiller en utilisant un seul article de chaque tiroir.

Remarque: cet exemple représente le nombre de feuilles dans un arbre décisionnel où number of children = depth Est effectué via 1 * 2 * 3 * .. * x.

0
Khaled.K

log (n) signifie croissance logarithmique. Un exemple serait diviser et conquérir des algorithmes. Si vous avez 1000 numéros triés dans un tableau (ex. 3, 10, 34, 244, 1203 ...) et que vous souhaitez rechercher un numéro dans la liste (trouver sa position), vous pouvez commencer par vérifier la valeur du paramètre. nombre à l’indice 500. S'il est inférieur à ce que vous recherchez, passez à 750. S'il est supérieur à ce que vous recherchez, passez à 250. Vous répétez ensuite le processus jusqu’à ce que vous trouviez votre valeur (et votre clé). Chaque fois que nous sautons la moitié de l'espace de recherche, nous pouvons essayer de tester de nombreuses autres valeurs car nous savons que le nombre 3004 ne peut pas être supérieur au nombre 5000 (rappelez-vous, il s'agit d'une liste triée).

n log (n) signifie alors n * log (n).

0
Statement