web-dev-qa-db-fra.com

Existe-t-il des cas où vous préféreriez un algorithme de complexité temporelle big-O plus élevé que l'algorithme inférieur?

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?

241
V.Leymarie

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:

  • la plupart du temps, la complexité de Big-O est plus difficile à atteindre et nécessite une implémentation qualifiée, de nombreuses connaissances et de nombreux tests.
  • big-O masque les détails d'une constante : l'algorithme exécuté dans 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.
  • big-O est logique lorsque vous calculez plus grand que quelque chose. Si vous devez trier un tableau de trois nombres, peu importe que vous utilisiez l'algorithme O(n*log(n)) ou O(n^2).
  • parfois, l'avantage de la complexité temporelle en minuscules peut être vraiment négligeable. Pour exemple, il existe un arbre de structure de données tango qui donne à 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.
  • la complexité temporelle n'est pas tout. Imaginez un algorithme qui s'exécute dans 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 algorithme
  • la parallélisation est une bonne fonctionnalité dans notre monde distribué. Il existe des algorithmes facilement parallélisables, et certains ne le sont pas du tout. Parfois, il est judicieux d’exécuter un algorithme sur 1 000 machines standard avec une complexité plus élevée que d’utiliser une machine avec une complexité légèrement supérieure.
  • dans certains endroits (sécurité), une complexité peut être requise. Personne ne veut avoir un algorithme de hachage capable de hacher extrêmement vite (car les autres personnes peuvent alors vous forcer brutalement plus vite)
  • bien que cela ne soit pas lié au changement de complexité, certaines des fonctions de sécurité doivent être écrites de manière à empêcher les attaques temporelles . Ils restent la plupart du temps dans la même classe de complexité, mais sont modifiés de manière à prendre toujours les pires cas pour faire quelque chose. Un exemple consiste à comparer que les chaînes sont égales. Dans la plupart des applications, il est judicieux d'interrompre le processus si les premiers octets sont différents, mais en sécurité, vous attendez toujours la fin pour annoncer la mauvaise nouvelle.
  • quelqu'un a breveté l'algorithme de moindre complexité et il est plus économique pour une entreprise d'utiliser une complexité plus élevée que de payer de l'argent.
  • certains algorithmes s'adaptent bien à des situations particulières. Le tri par insertion, par exemple, a une complexité temporelle moyenne de O(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.
264
Salvador Dali

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.

228
Alistra

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.

57
NoseKnowsAll

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 .

43
Kevin K

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.)

37
Loren Pechtel

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.

36
jpmc26

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.

23

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.

21
Simulant

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:

  1. Utilisez un jeu de bits de 1 000 000 bits
  2. Utiliser un tableau trié des entiers de la liste noire et utiliser une recherche binaire pour y accéder

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.

15
Philipp Claßen

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.

13
Warren Weckesser

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.

12
Mehrdad

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).

11
John Coleman

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.

10
uylmz

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.

9
Solomonoff's Secret

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 :

  1. Permuter le tableau au hasard en utilisant un processus quantique,
  2. Si le tableau n'est pas trié, détruisez l'univers.
  3. 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".

9
TheHansinator

À 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.

7
Dmitry Rubanovich

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.

6
user207421

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.

6
Madusudanan

Lorsque n est petit et que O(1) est constamment lent.

4
HoboBen
  1. lors de la refonte d'un programme, il s'avère qu'une procédure est optimisée avec O(1) au lieu de O (lgN), mais si ce n'est pas le goulot d'étranglement de ce programme, il est difficile de comprendre le O(1) alg. Alors vous n’auriez pas à utiliser l’algorithme O(1)
  2. quand O(1) a besoin de beaucoup de mémoire que vous ne pouvez pas fournir, alors que le temps de O(lgN) peut être accepté.
3
yanghaogn
  1. Lorsque l'unité "1" dans O(1) est très élevée par rapport à l'unité dans O (log n) et que la taille de jeu attendue est petite. Par exemple, il est probablement plus lent de calculer les codes de hachage du dictionnaire que d’itérer un tableau s’il n’ya que deux ou trois éléments.

o

  1. Lorsque la mémoire ou d’autres ressources non temporelles requises dans l’algorithme O(1) sont exceptionnellement grandes par rapport à l’algorithme O (log n).
3
Joel Coehoorn

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.

  • Le hachage des mots de passe est parfois ralenti de manière arbitraire afin de rendre plus difficile la tâche de deviner les mots de passe par force brute. Ceci poste de sécurité de l'information a une puce à ce sujet (et beaucoup plus).
  • Bit Coin utilise un problème lent à maîtriser pour un réseau d’ordinateurs à résoudre afin de "miner" les pièces de monnaie. Cela permet à la monnaie d'être extraite à un taux contrôlé par le système collectif.
  • Les chiffrements asymétriques (comme RSA ) sont conçus pour rendre le déchiffrement sans les clés intentionnellement lente afin d'empêcher que quelqu'un d'autre sans la clé privée ne déchiffre le chiffrement. Les algorithmes sont conçus pour être craqués dans l'espoir 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.

1
Frank Bryce