Question

J'ai remarqué une utilisation très étrange de O (1) dans la discussion sur les algorithmes impliquant le hachage et les types de recherche, souvent dans le contexte de l'utilisation d'un type de dictionnaire fourni par le système de langage ou de l'utilisation de types de dictionnaire ou de tableau de hachage utilisé en utilisant la notation tableau-index.

Fondamentalement, O (1) signifie lié par un temps constant et (généralement) un espace fixe. Certaines opérations assez fondamentales sont O (1), bien que l’utilisation de langages intermédiaires et de machines virtuelles spéciales ait tendance à déformer ceux qui pensent ici (par exemple, comment amortir le ramasse-miettes et d’autres processus dynamiques par rapport à ce qui serait autrement des activités O (1)).

Mais en ignorant l'amortissement des latences, le ramassage des ordures, etc., je ne comprends toujours pas comment passer à l'hypothèse que certaines techniques impliquant une sorte de recherche peuvent être O (1) sauf dans des conditions très spéciales.

Bien que je l’ai déjà remarqué, un exemple vient d’apparaître dans la question Pandincus, &"; Propre & # 8217; collection à utiliser pour obtenir des éléments dans le temps O (1) dans C # .NET? " .

Comme je l’ai fait remarquer ici, la seule collection que je connaisse qui fournit un accès O (1) en tant que liaison garantie est un tableau à liaisons fixes avec une valeur d’indice entier. La présomption est que le tableau est implémenté par un mappage vers une mémoire à accès aléatoire qui utilise des opérations O (1) pour localiser la cellule ayant cet index.

Pour les collections qui impliquent une sorte de recherche afin de déterminer l'emplacement d'une cellule correspondante pour un type d'index différent (ou pour un tableau fragmenté avec un index entier), la vie n'est pas si facile. En particulier, s'il y a des collisions et que la congestion est possible, l'accès n'est pas exactement O (1). Et si la collection est flexible, il faut reconnaître et amortir le coût d'expansion de la structure sous-jacente (telle qu'un arbre ou une table de hachage) pour lequel désengorgement (par exemple incidence élevée de collisions ou déséquilibre d'arbres) .

Je n'aurais jamais pensé parler de ces structures flexibles et dynamiques en tant que O (1). Pourtant, je les vois proposées comme des solutions O (1) sans identifier les conditions qui doivent être maintenues pour qu'un accès O (1) soit réellement assuré (et que cette constante soit négligeable).

LA QUESTION: Toute cette préparation est vraiment une question. Quelle est la désinvolture autour de O (1) et pourquoi est-il accepté si aveuglément? Est-il reconnu que même O (1) peut être excessivement grand, même s'il est presque constant? Ou bien O (1) est-il simplement l’appropriation d’une notion de complexité informatique à un usage informel? Je suis perplexe.

MISE À JOUR: Les réponses et commentaires indiquent à quel point j’étais décontracté lorsque je définissais moi-même O (1), et j’ai réparé cela. Je cherche toujours de bonnes réponses, et certains fils de commentaires sont plutôt plus intéressants que leurs réponses, dans quelques cas.

Était-ce utile?

La solution

D'après ce que je comprends, O (1) n'est pas nécessairement constant. au contraire, il ne dépend pas des variables considérées. On peut donc dire que la recherche de hachage est O (1) en ce qui concerne le nombre d'éléments dans le hachage, mais pas en ce qui concerne la longueur des données en cours de hachage ou le rapport entre les éléments et les compartiments dans le hachage.

L’autre élément de confusion est que la grande notation O décrit un comportement limitant. Ainsi, une fonction f (N) pour de petites valeurs de N peut en effet présenter une grande variation, mais vous auriez quand même raison de dire qu'il s'agit de O (1) si la limite lorsque N approche de l'infini est constante par rapport à N.

Autres conseils

Le problème est que les gens sont vraiment bâclés avec la terminologie. Il y a 3 classes importantes mais distinctes ici:

O (1) pire des cas

C’est simple: toutes les opérations ne prennent pas plus de temps dans le pire des cas, et donc dans tous les cas. L'accès à un élément d'un tableau est O(1) pire des cas.

O (1) pire des cas amortis

Amortisé signifie que toutes les opérations ne sont pas O(N) dans le pire des cas, mais pour toute séquence de N opérations, le coût total de la séquence n'est pas <=> dans le pire des cas. Cela signifie que même si nous ne pouvons pas réduire le coût d'une opération unique par une constante, il y aura toujours assez de & "Quick &"; opérations à rattraper pour le " lent " opérations telles que le temps d'exécution de la séquence d'opérations soit linéaire en nombre d'opérations.

Par exemple, le tableau dynamique standard qui double sa capacité lorsqu'il est rempli nécessite <= > temps amorti pour insérer un élément à la fin, même si certaines insertions nécessitent <=> du temps - il y a toujours suffisamment de <=> insertions pour que l'insertion de N éléments prenne toujours <=> la durée totale.

O (1) cas moyen

Celui-ci est le plus délicat. Il existe deux définitions possibles du cas moyen: une pour les algorithmes aléatoires à entrées fixes et une pour les algorithmes déterministes à entrées aléatoires.

Pour les algorithmes aléatoires avec des entrées fixes, nous pouvons calculer le temps de traitement de cas moyen pour toute entrée donnée en analysant l'algorithme et en déterminant la distribution de probabilité de tous les temps de fonctionnement possibles et en prenant la moyenne pour cette distribution (en fonction de l'algorithme, cela peut être impossible ou non en raison du problème d’arrêt).

Dans l’autre cas, nous avons besoin d’une distribution de probabilité sur les entrées. Par exemple, si nous devions mesurer un algorithme de tri, une distribution de probabilité de ce type serait la distribution qui contient tous les N! Les permutations possibles de l’entrée sont également probables. Ensuite, la durée moyenne de traitement est la durée moyenne de toutes les entrées possibles, pondérée par la probabilité de chaque entrée.

Puisque le sujet de cette question concerne les tables de hachage, qui sont déterministes, je vais me concentrer sur la deuxième définition du cas moyen. Maintenant, nous ne pouvons pas toujours déterminer la distribution de probabilité des entrées car, eh bien, nous pourrions tout hacher, et ces éléments pourraient provenir d'un utilisateur les saisissant ou d'un système de fichiers. Par conséquent, quand on parle de tables de hachage, la plupart des gens supposent simplement que les entrées sont bien comportées et que la fonction de hachage est bien comportée de sorte que la valeur de hachage de toute entrée est essentiellement distribuée de manière aléatoire de manière uniforme sur la plage de valeurs de hachage possibles.

Prenez un moment et laissez ce dernier point entrer dans le vif - la <=> performance moyenne des cas pour les tables de hachage provient de la supposition que toutes les valeurs de hachage sont uniformément distribuées. Si cette hypothèse est violée (ce qui n'est généralement pas le cas, mais cela peut et doit se produire), la durée d'exécution n'est plus <=> en moyenne.

Voir aussi Déni de service de la complexité algorithmique . Dans cet article, les auteurs expliquent comment ils ont exploité certaines des faiblesses des fonctions de hachage par défaut utilisées par deux versions de Perl pour générer un grand nombre de chaînes avec des collisions de hachage. Armés de cette liste de chaînes, ils ont généré une attaque par déni de service sur certains serveurs Web en les nourrissant de ces chaînes, ce qui a entraîné le pire comportement <=> dans les tables de hachage utilisées par les serveurs Web.

  

O (1) signifie temps constant et (généralement) espace fixe

Juste pour clarifier ce sont deux déclarations séparées. Vous pouvez avoir O (1) dans le temps mais O (n) dans l’espace ou autre chose.

  

Est-il reconnu que même O (1) peut être excessivement grand, même s'il est presque constant?

O (1) peut être impraticablement énorme et reste toujours O (1). On oublie souvent que si vous savez que vous avez un très petit ensemble de données, la constante est plus importante que la complexité et, pour des ensembles de données raisonnablement petits, il s'agit d'un équilibre entre les deux. Un algorithme O (n!) Peut surpasser O (1) si les constantes et les tailles des ensembles de données ont l’échelle appropriée.

La notation

O () est une mesure de la complexité - et non du temps nécessaire à un algorithme, ou une mesure pure de la façon dont & "; bien &"; un algorithme donné a un but donné.

Je vois ce que vous dites, mais je pense que deux hypothèses de base sous-tendent l'affirmation selon laquelle les recherches dans une table de hachage ont une complexité de O (1).

  • La fonction de hachage est raisonnablement conçue pour éviter un grand nombre de collisions.
  • L'ensemble des clés est distribué de manière assez aléatoire, ou du moins n'a pas été conçu exprès pour rendre la fonction de hachage mal performante.

La pire complexité d’une recherche dans une table de hachage est O (n), mais c’est extrêmement improbable compte tenu des deux hypothèses ci-dessus.

Hashtables est une structure de données qui prend en charge la recherche et l'insertion de O (1).

Une table de hachage a généralement une paire clé / valeur, la clé servant de paramètre à une fonction (a fonction de hachage ) qui déterminera l'emplacement de la valeur dans sa structure de données interne , généralement un tableau.

Comme l'insertion et la recherche ne dépendent que du résultat de la fonction de hachage et non de la taille de la table de hachage ni du nombre d'éléments stockés, une table de hachage comporte une insertion et une recherche de O (1).

Il existe cependant un avertissement . Autrement dit, à mesure que la table de hachage sera de plus en plus remplie, des collisions de hachage se produiront. la fonction de hachage retournera un élément d'un tableau déjà occupé. Cela nécessitera une résolution des conflits pour en trouver un autre. élément vide.

Lorsqu'une collision de hachage se produit, une recherche ou une insertion ne peut pas être effectuée en un temps O (1). Cependant, de bons algorithmes de résolution des collisions peuvent réduire le nombre de tentatives de trouver un autre emplacement vide approprié ou augmenter la taille de la table de hachage peut réduire le nombre de collisions en premier lieu.

Donc, en théorie, seulement une table de hachage adossée à un tableau avec un nombre infini d'éléments et une fonction de hachage parfaite pourrait atteindre la performance O (1) , car c'est le seul moyen pour éviter les collisions de hachage qui augmentent le nombre d'opérations requises. Par conséquent, tout tableau de taille finie sera, à un moment ou à un autre, inférieur à O (1) en raison de collisions de hachage.

Regardons un exemple. Utilisons une table de hachage pour stocker les (key, value) paires suivantes:

  • (Name, Bob)
  • (Occupation, Student)
  • (Location, Earth)

Nous allons implémenter le moteur de table de hachage avec un tableau de 100 éléments.

Le key sera utilisé pour déterminer un élément du tableau dans lequel stocker la paire (value, hash_function). Pour déterminer l'élément, le hash_function("Name") sera utilisé:

  • hash_function("Occupation") renvoie 18
  • hash_function("Location") renvoie 32
  • "Name" renvoie 74 .

À partir du résultat ci-dessus, nous allons affecter les ("Pet", "Dog") paires aux éléments du tableau.

array[18] = ("Name", "Bob")
array[32] = ("Occupation", "Student")
array[74] = ("Location", "Earth")

L'insertion nécessite uniquement l'utilisation d'une fonction de hachage et ne dépend pas de la taille de la hashtable ni de ses éléments. Elle peut donc être exécutée dans le temps O (1).

De même, la recherche d'un élément utilise la fonction de hachage.

Si nous voulons rechercher la clé hash_function("Pet"), nous allons effectuer un "Pet" pour déterminer quel élément du tableau réside dans la valeur souhaitée.

Par ailleurs, la recherche ne dépend pas de la taille de la table de hachage ni du nombre d'éléments stockés, par conséquent, une opération O (1).

Tout va bien. Essayons d'ajouter une entrée supplémentaire de <=>. Toutefois, il existe un problème, car <=> renvoie 18 , ce qui correspond au même hachage pour la <=> clé.

Par conséquent, nous devrons résoudre cette collision de hachage. Supposons que la fonction de résolution de collision par hachage utilisée indique que le nouvel élément vide est 29 :

array[29] = ("Pet", "Dog")

Puisqu'il y a eu une collision de hachage dans cette insertion, notre performance n'était pas tout à fait à O (1).

Ce problème se posera également lorsque nous essaierons de rechercher la clé <=>, car le fait de rechercher l'élément contenant la clé <=> en effectuant <=> renverra toujours 18 au départ.

Une fois que nous aurons recherché l'élément 18, nous trouverons la clé <=> plutôt que <=>. Lorsque nous trouverons cette incohérence, nous devrons résoudre la collision en ordrepour récupérer le bon élément contenant la clé <=> actuelle. Rétablir une collision de hachage est une opération supplémentaire qui empêche la hashtable de s'exécuter à l'heure O (1).

Je ne peux parler des autres discussions que vous avez vues, mais il existe au moins un algorithme de hachage pour lequel est garanti d'être O (1).

Le hachage Cuckoo conserve un invariant de sorte qu'il n'y ait pas d'enchaînement dans la table de hachage. L'insertion est amortie O (1), la récupération est toujours O (1). Je n'en ai jamais vu la mise en œuvre, c'est quelque chose qui vient d'être découvert quand j'étais au collège. Pour les ensembles de données relativement statiques, cela devrait être un très bon O (1), car il calcule deux fonctions de hachage, effectue deux recherches et connaît immédiatement la réponse.

Remarquez, cela suppose que le calcul du hachage est également O (1). Vous pourriez faire valoir que pour les chaînes de longueur K, tout hachage est au minimum O (K). En réalité, vous pouvez lier K assez facilement, disons K & Lt; 1000. O (K) ~ = O (1) pour K & Lt; 1000.

Il peut exister une erreur conceptuelle dans votre compréhension de la notation Big-Oh. Cela signifie que, compte tenu d'un algorithme et d'un jeu de données en entrée, la limite supérieure du temps d'exécution de l'algorithme dépend de la valeur de la fonction O lorsque la taille du jeu de données tend vers l'infini.

Quand on dit qu'un algorithme prend O (n) temps, cela signifie que l'exécution du pire cas d'un algorithme dépend linéairement de la taille de l'ensemble d'entrées.

Quand un algorithme prend O (1) temps, cela signifie seulement que, étant donné la fonction T (f) qui calcule le temps d’exécution d’une fonction f (n), il existe un nombre positif naturel k tel que T (f) < k pour toute entrée n. En gros, cela signifie que la limite supérieure du temps d’exécution d’un algorithme ne dépend pas de sa taille et a une limite fixe et finie.

Cela ne signifie en aucun cas que la limite est petite, mais indépendante de la taille de l'ensemble d'entrées. Donc, si je définis artificiellement une borne k pour la taille d’un ensemble de données, sa complexité sera alors O (k) == O (1).

Par exemple, la recherche d'une instance d'une valeur dans une liste liée est une opération O (n). Mais si je dis qu'une liste a au plus 8 éléments, alors O (n) devient O (8) devient O (1).

Dans ce cas, nous avons utilisé une trie structure de données comme dictionnaire (une arborescence de caractères, où le nœud feuille contient la valeur de la chaîne utilisée en tant que clé). Si la clé est liée, son temps de recherche peut être considéré comme O (1) (Si je définis un champ de caractères comme comprenant au maximum k caractères, une hypothèse raisonnable dans de nombreux cas).

Pour une table de hachage, aussi longtemps que vous supposez que la fonction de hachage est bonne (distribuée de manière aléatoire) et suffisamment fragmentée pour minimiser les collisions, et que le rehachage est effectué lorsque la structure de données est suffisamment dense, vous pouvez le considérer comme une O (1) structure de temps d'accès.

En conclusion, le temps O (1) peut être surestimé pour beaucoup de choses. Pour les structures de données volumineuses, la complexité d'une fonction de hachage adéquate peut ne pas être triviale et il existe suffisamment de cas dans lesquels le nombre de collisions l'amène à se comporter comme une structure de données O (n), et la reprise peut devenir extrêmement coûteuse. Dans ce cas, une structure O (log (n)) telle qu’AVL ou B-tree peut constituer une alternative supérieure.

En général, je pense que les gens les utilisent relativement sans se soucier de leur exactitude. Par exemple, les structures de données basées sur le hachage sont de type O (1) (moyenne), si elles sont bien conçues et que vous avez un bon hachage. Si tout se limite à un seul seau, alors c'est O (n). Généralement, bien que l'on utilise un bon algorithme et que les clés soient raisonnablement distribuées, il est pratique de parler de lui sous la forme O (1) sans toutes les qualifications. De même avec les listes, les arbres, etc. Nous pensons à certaines implémentations et il est tout simplement plus pratique de parler d’elles, lorsqu’il s’agit de généralités, sans qualifications. Si, en revanche, nous discutons d’implémentations spécifiques, il vaut probablement mieux être plus précis.

Les résultats de recherche dans HashTable correspondent à O (1) en ce qui concerne le nombre d'éléments dans le tableau, car peu importe le nombre d'éléments que vous ajoutez à la liste, le coût de hachage d'un seul élément est pratiquement identique, et la création du hash vous indiquera l'adresse de l'élément.

Pour répondre à la question de savoir si cela est pertinent: le PO a demandé pourquoi O (1) semblait être projeté de manière aussi désinvolte alors qu’il pensait que cela ne pouvait évidemment pas s’appliquer dans de nombreuses circonstances. Cette réponse explique que le temps O (1) est vraiment possible dans ces circonstances.

Les implémentations de table de hachage ne sont en pratique pas & "exactement &"; O (1) en cours d'utilisation. Si vous en testez une, vous constaterez qu'elles ont en moyenne environ 1,5 recherche pour trouver une clé donnée dans un grand ensemble de données

(en raison du fait que des collisions DO se produisent et lors de la collision, un emplacement différent doit être attribué ")

En outre, dans la pratique, les tableaux de hachage sont sauvegardés par des tableaux de taille initiale, & "cultivés &"; doubler la taille quand il atteint 70% de plénitude en moyenne, ce qui donne un espace d'adressage relativement bon. Après 70%, les taux de collision augmentent plus rapidement.

La théorie Big O stipule que si vous avez un algorithme O (1), voire même un algorithme O (2), le facteur critique est le degré de relation entre la taille de l'ensemble d'entrées et les étapes à suivre pour insérer / extraire l'un d'entre eux. . O (2) est toujours le temps constant, donc nous l'approchons simplement de O (1), car cela signifie plus ou moins la même chose.

En réalité, il n'existe qu'un seul moyen d'avoir une & "parfaite hashtable &"; avec O (1), et cela nécessite:

  1. Un générateur de clé de hachage parfait et global
  2. Un espace d'adressage non lié.

( Cas exceptionnel : si vous pouvez calculer à l'avance toutes les permutations de clés autorisées pour le système et si votre espace adresse de magasin de support cible est défini à la taille où il peut contenir toutes les clés sont autorisés, alors vous pouvez avoir un hachage parfait, mais c'est un & "domaine limité &"; perfection)

Dans le cas d’une allocation de mémoire fixe, il n’est nullement plausible de l’avoir, car cela supposerait que vous disposiez d’un moyen magique pour emballer une quantité infinie de données dans un espace fixe sans perte de données, et c'est impossible sur le plan logistique.

Donc rétrospectivement, obtenant O (1.5) qui est toujours un temps constant, dans une quantité finie de mémoire avec même un générateur de clé de hachage relativement Na & # 239, j’estime sacrément génial.

Note suffixive Remarque J'utilise O (1.5) et O (2) ici. Ceux-ci n'existent pas réellement dans big-o. Celles-ci sont simplement ce que les gens qui ne connaissent pas big-o supposent en être la raison.

Si quelque chose prend 1,5 pas pour trouver une clé, ou 2 pas pour trouver cette clé, ou 1 pas pour trouver cette clé, mais que le nombre d’étapes n’excède jamais 2 et qu’il faut 1 pas ou 2 est complètement aléatoire, alors c'est toujours Big-O of O (1). En effet, peu importe le nombre d'éléments que vous ajoutez à la taille du jeu de données, il conserve les étapes & Lt; 2. Si pour toutes les tables & Gt; 500 touches il faut 2 étapes, alors vous pouvez supposer que ces 2 étapes sont en fait une étape avec 2 parties, ... qui est toujours O (1).

Si vous ne pouvez pas faire cette hypothèse, alors vous n’êtes pas du tout en train de penser, car vous devez alors utiliser le nombre qui représente le nombre d’étapes de calcul finies requises pour tout faire et & "One- étape " n'a aucun sens pour vous. N'oubliez pas qu'il existe une NON corrélation directe entre Big-O et le nombre de cycles d'exécution impliqués.

O (1) signifie exactement que la complexité temporelle de l'algorithme est limitée par une valeur fixe. Cela ne signifie pas qu'elle est constante, mais seulement qu'elle est liée quelles que soient les valeurs d'entrée. Strictement parlant, de nombreux algorithmes de temps supposés O (1) ne sont pas réellement O (1) et vont si lentement qu'ils sont liés pour toutes les valeurs d'entrée pratiques.

Oui, la récupération de place affecte la complexité asymptotique des algorithmes exécutés dans l’arène de récupération de place. Ce n’est pas sans coût, mais il est très difficile à analyser sans méthodes empiriques, car les coûts d’interaction ne sont pas compositionnels.

Le temps passé à la collecte des déchets dépend de l'algorithme utilisé. Généralement, les éboueurs modernes basculent entre les modes lorsque la mémoire se remplit pour maîtriser ces coûts. Par exemple, une approche courante consiste à utiliser un collecteur de copie de style Cheney lorsque la pression de la mémoire est faible, car son coût est proportionnel à la taille de la scène, en contrepartie de l'utilisation de plus d'espace, et de basculer vers un collecteur de marques et balayages lorsque la pression de la mémoire devient plus grand, parce que même si cela paie un coût proportionnel au set vivant pour le marquage et à tout le tas ou le set fixe pour le balayage. Au moment où vous ajoutez le marquage des cartes et d’autres optimisations, etc., les coûts les plus défavorables pour un ramasse-miettes pratique peuvent en réalité être bien pires, car il en résulte un facteur logarithmique supplémentaire pour certaines habitudes d'utilisation.

Ainsi, si vous allouez une grande table de hachage, même si vous y accédez à l'aide de la recherche O (1) pendant toute sa durée de vie, si vous le faites dans un environnement nettoyé, le ramasse-miettes parcourt parfois le tableau entier. , parce que sa taille est O (n) et que vous devrez payer ce coût périodiquement lors de la collecte.

Si nous omettons généralement l'analyse de complexité des algorithmes, c'est que la récupération de place interagit avec votre algorithme de manière non triviale. Son coût dépend beaucoup de ce que vous faites dans le même processus. L'analyse n'est donc pas compositionnelle.

De plus, au-delà du problème de la copie, de la copie, du marquage et du balayage, les détails de la mise en œuvre peuvent avoir une incidence considérable sur la complexité qui en résulte:

  1. Les éboueurs incrémentiels qui suivent les morceaux sales, etc. peuvent tout faire disparaître.
  2. Cela dépend si votre CPG fonctionne périodiquement en fonction de l'heure de l'horloge murale ou s'il est proportionnel au nombre d'allocations.
  3. Si un algorithme de style de balise et de balayage est simultané ou stop-the-world
  4. Indique si les nouvelles allocations sont marquées en noir si elles restent blanches jusqu'à ce qu'elles tombent dans un conteneur noir.
  5. Si votre langue admet les modifications des pointeurs, certains éboueurs peuvent travailler en un seul passage.

Enfin, lorsque nous discutons d’un algorithme, nous discutons d’un homme de paille. L’asymptotique n’intègrera jamais toutes les variables de votre environnement. Il est rare que vous implémentiez tous les détails d'une structure de données telle que conçue. Vous empruntez une fonctionnalité ici et là, vous déposez une table de hachage parce que vous avez besoin d'un accès rapide à une clé non ordonnée, vous utilisez une recherche d'union sur des ensembles disjoints avec compression de chemin et union par rang pour fusionner des régions de mémoire parce que vous ne pouvez pas. vous permettre de payer un coût proportionnel à la taille des régions lorsque vous les fusionnez ou ce que vous avez. Ces structures sont considérées comme des primitives et les asymptotiques vous aident lors de la planification des caractéristiques de performance globales de la structure "globale", mais il est également important de connaître la nature des constantes.

Vous pouvez implémenter cette table de hachage avec des caractéristiques parfaitement asymptotiques O (1), mais n'utilisez pas la récupération de place; mappez-le en mémoire à partir d'un fichier et gérez-le vous-même. Cependant, vous n’aimerez probablement pas les constantes impliquées.

Je pense que lorsque beaucoup de gens parlent du terme & "; O (1) &"; ils ont implicitement en tête un & "petit &"; constante, quelle que soit & "; petite &"; signifie dans leur contexte.

Vous devez prendre toute cette grande analyse avec contexte et bon sens. Cela peut être un outil extrêmement utile ou ridicule, selon votre utilisation.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top