Existe-t-il des cas où vous préféreriez O(log n)
complexité temporelle à O(1)
complexité temporelle? Ou O(n)
to O(log n)
?
Avez-vous des exemples?
Il peut y avoir de nombreuses raisons de préférer un algorithme avec une complexité de temps O plus grande que la plus basse:
10^5
est meilleur du point de vue big-O que 1/10^5 * log(n)
_ (O(1)
vs O(log(n)
), mais pour les plus raisonnables n
le premier fonctionnera mieux. Par exemple, la meilleure complexité pour la multiplication matricielle est O(n^2.373)
, mais la constante est tellement élevée que les bibliothèques informatiques (à ma connaissance) ne l'utilisent pas.O(n*log(n))
ou O(n^2)
.O(log log N)
la complexité du temps nécessaire pour trouver un élément, mais il existe également un arbre binaire qui le trouve dans O(log n)
. Même pour un nombre considérable de n = 10^20
, la différence est négligeable.O(n^2)
et qui nécessite O(n^2)
de la mémoire. Cela peut être préférable à O(n^3)
time et à O(1)
space lorsque le n n'est pas très gros. Le problème est que vous pouvez attendre longtemps, mais vous doutez fort que vous puissiez trouver un RAM assez grand pour l'utiliser avec votre algorithmeO(n^2)
, pire que le tri rapide ou le mergesort, mais en tant que algorithme en ligne , il peut efficacement trier une liste de valeurs au fur et à mesure de leur réception ( en tant qu'utilisateur) où la plupart des autres algorithmes ne peuvent fonctionner efficacement que sur une liste complète de valeurs.Il y a toujours la constante cachée, qui peut être inférieure sur l'algorithme O (log n). Cela permet donc de travailler plus rapidement dans la pratique avec des données réelles.
Il existe également des problèmes d'espace (par exemple, fonctionner sur un grille-pain).
Il y a aussi le souci du temps du développeur - O (log n) peut être 1 000 × plus facile à implémenter et à vérifier.
Je suis surpris que personne n'ait encore mentionné les applications liées à la mémoire.
Il peut exister un algorithme comportant moins d’opérations en virgule flottante en raison de sa complexité (par exemple, O (1) <O (log n)) ou parce que la constante devant la complexité est plus petite (ie 2n2 <6n2). Quoi qu'il en soit, vous préférerez peut-être l'algorithme avec plus de FLOP si l'algorithme inférieur de FLOP est davantage lié à la mémoire.
Ce que j'entends par "lié à la mémoire", c'est que vous accédez souvent à des données constamment hors de cache. Pour pouvoir récupérer ces données, vous devez extraire la mémoire de votre espace mémoire réel dans votre cache avant de pouvoir effectuer votre opération dessus. Cette étape de récupération est souvent assez lente, beaucoup plus lente que votre opération elle-même.
Par conséquent, si votre algorithme nécessite davantage d’opérations (et pourtant, ces opérations sont effectuées sur des données déjà en cache (et ne nécessitent donc pas d’extraction)], il surperformera toujours votre algorithme avec moins d’opérations (qui doivent être effectuées en sortie). -cache data [et nécessite donc une extraction]) en termes de temps réel du mur.
Dans les contextes où la sécurité des données est une préoccupation, un algorithme plus complexe peut être préférable à un algorithme moins complexe si l'algorithme plus complexe a une meilleure résistance à attaques temporelles .
Alistra a bien compris, mais n'a fourni aucun exemple, alors je le ferai.
Vous avez une liste de 10 000 UPC codes pour ce que vend votre magasin. CUP à 10 chiffres, nombre entier pour le prix (prix en sous) et 30 caractères de description pour le reçu.
Approche O (log N): vous avez une liste triée. 44 octets si ASCII, 84 si Unicode. Sinon, traitez le UPC comme un int64 et vous obtenez 42 et 72 octets. 10 000 enregistrements - dans le cas le plus élevé, vous disposez d'un peu moins d'un mégaoctet d'espace de stockage.
Approche O (1): ne stockez pas le code UPC, vous l'utilisez plutôt comme une entrée dans le tableau. Dans le cas le plus bas, vous disposez de presque un tiers de téraoctet de stockage.
L'approche que vous utilisez dépend de votre matériel. Sur la plupart des configurations modernes raisonnables, vous utiliserez l'approche log N. Je peux imaginer que la deuxième approche est la bonne réponse si, pour une raison quelconque, vous travaillez dans un environnement où RAM est extrêmement court mais vous avez beaucoup de mémoire de masse. Un tiers de téraoctet sur un disque n’a rien de grave, il est utile de disposer de vos données dans une sonde du disque. L'approche binaire simple prend 13 en moyenne. (Notez cependant qu'en regroupant vos clés, vous pouvez obtenir 3 lectures garanties et, dans la pratique, vous mettriez en cache la première.)
Considérons un arbre rouge-noir. Il a accès, recherche, insertion et suppression de O(log n)
. Comparez à un tableau qui a accès à O(1)
et le reste des opérations est O(n)
.
Donc, dans le cas d’une application dans laquelle nous insérons, supprimons ou recherchons plus souvent que nous n’avons accès et d’un choix entre ces deux structures uniquement, nous préférerions l’arbre rouge-noir. Dans ce cas, vous pourriez dire que nous préférons le O(log n)
temps d'accès plus lourd de l'arbre rouge-noir.
Pourquoi? Parce que l'accès n'est pas notre préoccupation primordiale. Nous faisons des compromis: les performances de notre application sont davantage influencées par des facteurs autres que celui-ci. Nous permettons à cet algorithme de dégrader les performances car nous réalisons des gains importants en optimisant d’autres algorithmes.
La réponse à votre question est donc simplement la suivante: lorsque le taux de croissance de l’algorithme n’est pas celui que nous souhaitons optimiser, lorsque nous souhaitons optimiser autre chose . Toutes les autres réponses sont des cas particuliers de cela. Parfois, nous optimisons le temps d'exécution d'autres opérations. Parfois, nous optimisons pour la mémoire. Parfois, nous optimisons pour la sécurité. Parfois, nous optimisons la maintenabilité. Parfois, nous optimisons le temps de développement. Même la constante primordiale étant suffisamment faible pour avoir de l'importance, c'est l'optimisation du temps d'exécution lorsque vous savez que le taux de croissance de l'algorithme n'est pas l'impact le plus important sur le temps d'exécution. (Si votre ensemble de données se situait en dehors de cette plage, vous optimiseriez le taux de croissance de l’algorithme, car il finirait par dominer la constante.) Tout a un coût, et dans de nombreux cas, nous négocions le coût d’un taux de croissance plus élevé pour le système. algorithme pour optimiser quelque chose d'autre.
Oui.
Dans un cas réel, nous avons effectué des tests sur les recherches de table avec des clés de chaîne courtes et longues.
Nous avons utilisé un std::map
, un std::unordered_map
avec un hachage échantillonné 10 fois au maximum sur la longueur de la chaîne (nos clés ont tendance à ressembler à guid, donc c'est décent), et un hachage qui échantillonne chaque caractère (en théorie, les collisions réduites), un vecteur non trié dans lequel nous comparons un ==
et, si je me souviens bien, un vecteur non trié dans lequel nous stockons également un hachage, comparons d'abord le hachage, puis comparez les caractères .
Ces algorithmes vont de O(1)
(unordered_map) à O(n)
(recherche linéaire).
Pour un N de taille modeste, le O(n) bat souvent le O (1). Nous pensons que cela est dû au fait que les conteneurs basés sur des noeuds ont nécessité que notre ordinateur saute davantage en mémoire, contrairement aux conteneurs basés sur une base linéaire.
O(lg n)
existe entre les deux. Je ne me souviens pas comment ça s'est passé.
La différence de performance n’était pas si importante, et sur des ensembles de données plus importants, celle basée sur le hachage s’est beaucoup mieux comportée. Nous nous sommes donc contentés de la carte non ordonnée basée sur le hachage.
En pratique, pour une taille raisonnable n, O(lg n)
est O(1)
. Si votre ordinateur ne peut contenir que 4 milliards d'entrées dans votre table, alors O(lg n)
est délimité ci-dessus par 32
. (lg (2 ^ 32) = 32) (en informatique, lg est un raccourci pour log based 2).
En pratique, les algorithmes lg (n) sont plus lents que les algorithmes O(1), non pas à cause du facteur de croissance logarithmique, mais parce que la partie lg (n) signifie généralement que l'algorithme présente un certain niveau de complexité. , et cette complexité ajoute un facteur constant plus grand que n'importe quelle "croissance" du terme lg (n).
Cependant, les algorithmes complexes O(1) (comme le mappage de hachage) peuvent facilement avoir un facteur constant similaire ou supérieur.
La possibilité d'exécuter un algorithme en parallèle.
Je ne sais pas s'il existe un exemple pour les classes O(log n)
et O(1)
, mais pour certains problèmes, vous choisissez un algorithme avec une classe de complexité plus élevée lorsqu'il est plus facile de l'exécuter en parallèle.
Certains algorithmes ne peuvent pas être parallélisés mais ont une classe de complexité si faible. Considérons un autre algorithme qui produit le même résultat et peut être facilement mis en parallèle, mais possède une classe de complexité plus élevée. Lorsqu'il est exécuté sur une machine, le deuxième algorithme est plus lent, mais lorsqu'il est exécuté sur plusieurs machines, le temps d'exécution réel diminue et diminue alors que le premier algorithme ne peut pas accélérer.
Supposons que vous implémentez une liste noire sur un système intégré, où des nombres compris entre 0 et 1 000 000 pourraient être mis sur liste noire. Cela vous laisse deux options possibles:
L'accès au jeu de bits aura un accès constant. En termes de complexité temporelle, c'est optimal. À la fois d’un point de vue théorique et d’un point de vue pratique (c’est O(1) avec un temps système extrêmement faible et constant).
Néanmoins, vous voudrez peut-être préférer la deuxième solution. Surtout si vous vous attendez à ce que le nombre d'entiers sur la liste noire soit très petit, car la mémoire sera plus efficace.
Et même si vous ne développez pas pour un système embarqué où la mémoire est rare, je peux simplement augmenter la limite arbitraire de 1 000 000 à 1 000 000 000 000 et présenter le même argument. Le jeu de bits nécessiterait alors environ 125 Go de mémoire. Avoir une complexité garantie dans le pire des cas, O(1), pourrait ne pas convaincre votre patron de vous fournir un serveur aussi puissant.
Ici, je préférerais fortement une recherche binaire (O (log n)) ou un arbre binaire (O (log n)) sur le jeu de bits O(1). Et probablement, une table de hachage avec sa complexité dans le pire des cas de O(n) va les battre dans la pratique.
Ma réponse ici Sélection aléatoire rapide pondérée sur toutes les lignes d'une matrice stochastique est un exemple où un algorithme de complexité O(m) est plus rapide qu'un algorithme de complexité O (log (m )), quand m
n'est pas trop gros.
Les gens ont déjà répondu à votre question exacte, alors je vais aborder une question légèrement différente à laquelle les gens pourraient penser en venant ici.
De nombreux algorithmes et structures de données "O (1) time" ne prennent en réalité que attendu O(1) time, ce qui signifie que leur temps d'exécution moyen est O (1), éventuellement uniquement sous certaines hypothèses.
Exemples courants: Tables de hachage, développement de "listes de tableaux" (tableaux./a.a. de taille dynamique).
Dans de tels scénarios, vous préférerez peut-être utiliser des structures de données ou des algorithmes dont le temps est garanti absolument logarithmiquement liés, même s'ils ont de moins bonnes performances.
Un exemple pourrait donc être un arbre de recherche binaire équilibré, dont la durée d'exécution est pire en moyenne mais meilleure dans le pire des cas.
Une question plus générale est de savoir s'il existe des situations dans lesquelles on préférerait un algorithme O(f(n))
à un algorithme O(g(n))
alors que g(n) << f(n)
comme n
tend vers l'infini. Comme d'autres l'ont déjà mentionné, la réponse est clairement "oui" dans le cas où f(n) = log(n)
et g(n) = 1
. C'est parfois oui même dans le cas où f(n)
est polynomial mais g(n)
est exponentiel. Un exemple célèbre et important est celui de Algorithme Simplex pour la résolution de problèmes de programmation linéaire. Dans les années 1970, il s’est avéré que c’était O(2^n)
. Ainsi, son comportement dans le pire des cas est infaisable. Mais - son comportement moyen est extrêmement bon, même pour des problèmes pratiques avec des dizaines de milliers de variables et de contraintes. Dans les années 1980, des algorithmes temporels polynomiaux (tels que algorithme du point intérieur de Karmarkar ) pour la programmation linéaire ont été découverts, mais 30 ans plus tard, l'algorithme simplex semble toujours être l'algorithme de choix (à l'exception de certains très grands problèmes). C’est la raison évidente pour laquelle le comportement de cas moyen est souvent plus important que le comportement de cas pire, mais aussi pour une raison plus subtile que l’algorithme simplex est en quelque sorte plus informatif (par exemple, les informations de sensibilité sont plus faciles à extraire).
Pour mettre mes 2 cents dans:
Parfois, un algorithme moins complexe est sélectionné à la place d'un meilleur, lorsqu'il fonctionne sur un environnement matériel donné. Supposons que notre algorithme O(1) accède de manière non séquentielle à tous les éléments d'un très grand tableau de taille fixe pour résoudre notre problème. Placez ensuite ce module sur un disque dur mécanique ou sur une bande magnétique.
Dans ce cas, l'algorithme O(logn) (supposons qu'il accède au disque séquentiellement), devient plus favorable.
Il existe un bon cas d'utilisation de l'algorithme O(log(n)) au lieu d'un algorithme O(1) que les nombreuses autres réponses ont ignoré: l'immuabilité. Les cartes de hachage ont O(1) met et obtient, en supposant une bonne distribution des valeurs de hachage, mais elles nécessitent un état mutable. Les cartes d'arbre immuables ont O(log(n)) met et obtient, ce qui est asymptotiquement plus lent. Cependant, l’immuabilité peut être suffisamment précieuse pour compenser une performance inférieure et dans le cas où plusieurs versions de la carte doivent être conservées, l’immuabilité vous permet d’éviter de copier la carte, qui est O (n), et peut donc - améliorer performance.
Tout simplement parce que le coefficient - les coûts associés à la configuration, au stockage et à la durée d'exécution de cette étape - peut être beaucoup, beaucoup plus grand avec un problème big-O plus petit qu'avec un problème plus grand. Big-O est seulement une mesure de l'évolutivité des algorithmes .
Prenons l'exemple suivant du dictionnaire Hacker, proposant un algorithme de tri reposant sur le interprétation de plusieurs mécanismes de la mécanique quantique :
- Permuter le tableau au hasard en utilisant un processus quantique,
- Si le tableau n'est pas trié, détruisez l'univers.
- Tous les univers restants sont maintenant triés [y compris celui dans lequel vous vous trouvez].
(Source: http://catb.org/~esr/jargon/html/B/bogo-sort.html )
Notez que le gros-O de cet algorithme est O(n)
, qui bat tous les algorithmes de tri connus à ce jour sur les articles génériques. Le coefficient du pas linéaire est également très faible (puisqu'il ne s'agit que d'une comparaison, pas d'un échange, il est effectué de manière linéaire). Un algorithme similaire pourrait en fait être utilisé pour résoudre tout problème à la fois NP et co-NP en temps polynomial, puisque chaque solution possible (ou toute preuve il n’existe pas de solution) peut être généré à l’aide du processus quantique, puis vérifié en temps polynomial.
Cependant, dans la plupart des cas, nous ne voulons probablement pas prendre le risque que plusieurs mondes ne soient pas corrects, sans compter que l'acte de mettre en œuvre l'étape 2 reste "un exercice pour le lecteur".
À tout moment, lorsque n est lié et que le multiplicateur constant de O(1) algorithme est supérieur à la limite sur log (n). Par exemple, le stockage des valeurs dans un hachage est O (1), mais peut nécessiter un calcul coûteux d'une fonction de hachage. Si les éléments de données peuvent être comparés de manière triviale (par rapport à un certain ordre) et que la limite sur n est telle que log n est significativement inférieur au calcul du hachage sur un élément, le stockage dans un arbre binaire équilibré peut être plus rapide que le stockage dans un hashset.
Dans une situation en temps réel où vous avez besoin d’une limite supérieure ferme, sélectionnez par exemple. Heapsort, par opposition à Quicksort, car le comportement moyen de Heapsort est également son comportement le plus défavorable.
Ajout aux réponses déjà bonnes. Un exemple pratique serait index de hachage vs index B-tree dans la base de données Postgres.
Les index de hachage forment un index de table de hachage permettant d'accéder aux données du disque, tandis que btree, comme son nom l'indique, utilise une structure de données Btree.
Dans Big-O time, il s’agit de O(1) vs O (logN).
Les index de hachage sont actuellement déconseillés dans Postgres car, dans la vie réelle, en particulier dans les systèmes de base de données, obtenir un hachage sans collision est très difficile (peut conduire à une O(N) pire est encore plus difficile de les protéger contre les collisions (appelé journalisation en écriture anticipée - WAL dans postgres).
Ce compromis est fait dans cette situation puisque O(logN) convient assez bien pour les index et que mettre en œuvre O(1) est assez difficile et que le décalage horaire n'aurait pas vraiment d'importance.
Lorsque n
est petit et que O(1)
est constamment lent.
o
C'est souvent le cas pour les applications de sécurité pour lesquelles nous voulons concevoir des problèmes dont les algorithmes sont délibérément lents afin d'empêcher quelqu'un d'obtenir une réponse à un problème trop rapidement.
Voici quelques exemples qui me viennent à l’esprit.
O(2^n)
time où n
est la longueur en bits de la clé (il s'agit de la force brute).Ailleurs dans CS, Quick Sort est O(n^2)
dans le pire des cas, mais dans le cas général est O(n*log(n))
. Pour cette raison, l'analyse "Big O" n'est parfois pas la seule chose qui compte pour l'analyse de l'efficacité d'un algorithme.