Il semble de notoriété publique que les tables de hachage peuvent atteindre O (1), mais cela n'a jamais eu de sens pour moi. Quelqu'un peut-il s'il vous plaît expliquer? Voici deux situations qui me viennent à l’esprit:
A. La valeur est un int plus petit que la taille de la table de hachage. Par conséquent, la valeur est son propre hachage, il n'y a donc pas de table de hachage. Mais s'il y en avait, ce serait O(1) et serait toujours inefficace.
B. Vous devez calculer un hachage de la valeur. Dans cette situation, l'ordre est O(n) pour la taille des données recherchées. La recherche pourrait être O(1) une fois que vous avez fait O(n) fonctionne, mais cela continue de produire O(n) à mes yeux.
Et sauf si vous avez un hachage parfait ou une grande table de hachage, il y a probablement plusieurs éléments par seau. Donc, il y a quand même une petite recherche linéaire.
Je pense que les tables de hachage sont géniales, mais je n’obtiens pas la désignation O(1) sauf s’il est supposé être théorique.
Les articles sur les tables de hachage de Wikipedia font systématiquement référence au temps de recherche constant et ignorent totalement le coût de la fonction de hachage. Est-ce vraiment une mesure juste?
Edit: Pour résumer ce que j'ai appris:
C’est techniquement vrai, car la fonction de hachage n’est pas obligée d’utiliser toutes les informations de la clé; elle peut donc être constante et une table suffisamment grande peut réduire les collisions à une quasi-constante.
C’est vrai dans la pratique, car avec le temps, cela fonctionne bien tant que la fonction de hachage et la taille de la table sont choisies pour minimiser les collisions, même si cela implique souvent de ne pas utiliser de fonction de hachage à temps constant.
Vous avez deux variables ici, m et n, où m est la longueur de l'entrée et n le nombre d'éléments dans le hachage.
L'allégation de performance de recherche O(1) repose sur au moins deux hypothèses:
Si vos objets ont une taille variable et qu'un contrôle d'égalité nécessite d'examiner tous les bits, les performances deviennent alors O (m). La fonction de hachage n'a cependant pas besoin d'être O(m) - elle peut être O (1). Contrairement à un hachage cryptographique, une fonction de hachage à utiliser dans un dictionnaire n'a pas à examiner tous les bits de l'entrée pour calculer le hachage. Les implémentations sont libres de ne regarder qu'un nombre fixe de bits.
Pour un nombre suffisant d'éléments, le nombre d'éléments deviendra supérieur au nombre de hachages possibles et vous obtiendrez des collisions entraînant une augmentation des performances supérieure à O (1), par exemple O(n) pour une simple liste chaînée. traversal (ou O (n * m) si les deux hypothèses sont fausses).
En pratique, bien que techniquement fausse, la revendication O(1) soit approximativement vraie pour de nombreuses situations du monde réel, et en particulier pour celles où les hypothèses précédentes sont vérifiées.
Vous devez calculer le hachage, donc l'ordre est O(n) pour la taille des données recherchées. La recherche pourrait être O(1) une fois que vous avez fait O(n) fonctionne, mais cela continue de produire O(n) à mes yeux.
Quoi? Le hachage d'un seul élément prend un temps constant. Pourquoi serait-ce autre chose? Si vous insérez des éléments n
, alors oui, vous devez calculer des hachages n
, et cela prend du temps linéaire ... pour rechercher un élément, vous calculez un seul hachage de ce que vous recherchez, puis recherchez le compartiment approprié. avec ça. Vous ne recalculez pas les hachages de tout ce qui se trouve déjà dans la table de hachage.
Et à moins que vous n'ayez un hachage parfait ou une grande table de hachage, il y a probablement plusieurs éléments par seau, ce qui entraîne une petite recherche linéaire à un moment donné.
Pas nécessairement. Les compartiments ne doivent pas nécessairement être des listes ou des tableaux, ils peuvent être n'importe quel type de conteneur, tel qu'un BST équilibré. Cela signifie O(log n)
pire des cas. Mais c’est pourquoi il est important de choisir une bonne fonction de hachage afin d’éviter de placer trop d’éléments dans un même seau. Comme KennyTM l'a souligné, en moyenne, vous aurez toujours le temps O(1)
, même si vous devez parfois creuser dans un seau.
Le compromis entre les tables de hachage est bien sûr la complexité de l'espace. Vous échangez de la place pour le temps, ce qui semble être le cas habituel en informatique.
Vous mentionnez l'utilisation de chaînes comme clés dans l'un de vos autres commentaires. Vous êtes préoccupé par le temps qu'il faut pour calculer le hachage d'une chaîne, car celle-ci se compose de plusieurs caractères? Comme quelqu'un l'a fait remarquer à nouveau, vous n'avez pas nécessairement besoin d'examiner tous les caractères pour calculer le hachage, bien que cela puisse produire un meilleur hachage si vous le faites. Dans ce cas, s'il y a en moyenne des caractères m
dans votre clé et que vous les avez tous utilisés pour calculer votre hachage, alors je suppose que vous avez raison, les recherches prendraient O(m)
. Si m >> n
, vous pourriez avoir un problème. Vous seriez probablement mieux avec une BST dans ce cas. Ou choisissez une fonction de hachage moins chère.
Le hachage est de taille fixe - la recherche du compartiment de hachage approprié est une opération à coût fixe. Cela signifie que c'est O (1).
Le calcul du hachage ne doit pas nécessairement être une opération particulièrement coûteuse - nous ne parlons pas ici de fonctions de hachage cryptographiques. Mais c'est à propos. Le calcul de la fonction de hachage lui-même ne dépend pas du nombre n des éléments; bien que cela puisse dépendre de la taille des données d’un élément, ce n’est pas ce à quoi n se réfère. Donc, le calcul du hachage ne dépend pas de n et est également O (1).
Le hachage est O(1) uniquement s'il existe un nombre constant de clés dans la table et que d'autres hypothèses sont formulées. Mais dans de tels cas, cela a un avantage.
Si votre clé a une représentation sur n bits, votre fonction de hachage peut utiliser 1, 2, ... n de ces bits. Penser à une fonction de hachage qui utilise 1 bit. L'évaluation est O(1) pour sûr. Mais vous ne faites que partitionner l'espace de clé en 2. Vous mappez donc jusqu'à 2 ^ (n-1) clés dans le même chutier. En utilisant la recherche BST, il faut n-1 étapes pour localiser une clé particulière si elle est presque pleine.
Vous pouvez étendre ceci pour voir que si votre fonction de hachage utilise K bits, la taille de votre bin est de 2 ^ (n-k).
donc fonction de hachage K-bit ==> pas plus de 2 ^ K bacs effectifs ==> jusqu'à 2 ^ (n-K) clés à n bits par bin ==> (n-K) étapes (BST) pour résoudre les collisions. En fait, la plupart des fonctions de hachage sont beaucoup moins "efficaces" et nécessitent/utilisent plus de K bits pour produire 2 ^ k bin. Donc, même cela est optimiste.
Vous pouvez le voir de cette façon - vous aurez besoin de n étapes pour pouvoir distinguer de manière unique une paire de clés de n bits dans le pire des cas. Il n'y a vraiment aucun moyen de contourner cette limite de la théorie de l'information, table de hachage ou non.
Cependant, ce n'est PAS comment/quand vous utilisez la table de hachage!
L’analyse de complexité suppose que pour les clés à n bits, vous pouvez avoir 0 (2 ^ n) clés dans la table (par exemple, 1/4 de toutes les clés possibles). Mais la plupart du temps, si ce n’est tout le temps, nous utilisons une table de hachage, nous n’avons qu’un nombre constant de clés à n bits dans la table. Si vous voulez seulement un nombre constant de clés dans la table, disons que C est votre nombre maximum, vous pouvez alors former une table de hachage de O(C) bacs, qui garantit une collision constante attendue (avec une bonne fonction de hachage) ; et une fonction de hachage utilisant ~ logC des n bits de la clé. Chaque requête est alors O(logC) = O (1). C’est ainsi que les gens disent "l’accès à la table de hachage est O (1)" /
Il y a quelques pièges ici - premièrement, dire que vous n'avez pas besoin de tous les bits peut n'être qu'un truc de facturation. Premièrement, vous ne pouvez pas vraiment transmettre la valeur de clé à la fonction de hachage, car cela déplacerait n bits dans la mémoire, ce qui correspond à O (n). Donc, vous devez faire par exemple un passage de référence. Mais vous devez toujours le stocker quelque part déjà, opération qui était une opération O(n); il suffit de ne pas le facturer au hachage; votre tâche de calcul globale ne peut pas l'éviter. Deuxièmement, vous effectuez le hachage, trouvez le bac et vous avez trouvé plus d'une clé; votre coût dépend de votre méthode de résolution - si vous effectuez une comparaison (BST ou List), vous aurez l'opération O(n) (la clé de rappel est à n bits); si vous faites 2nd hash, eh bien, vous avez le même problème si 2nd hash est en collision. Donc, O(1) n'est pas garanti à 100% sauf en l'absence de collision (vous pouvez améliorer les chances en disposant une table avec plus de bacs que de clés, mais quand même).
Considérez l’alternative, par exemple BST, dans ce cas. il y a des touches C, de sorte qu'un BST équilibré aura une profondeur de O(logC), de sorte qu'une recherche prend des étapes O(logC). Cependant, la comparaison dans ce cas serait une opération O(n) ... il apparaît donc que le hachage est un meilleur choix dans ce cas.
A. La valeur est un int plus petit que la taille de la table de hachage. Par conséquent, la valeur est son propre hash, il n'y a donc pas de table de hash. Mais s'il y en avait, ce serait O(1) et resterait inefficace.
Dans ce cas, vous pouvez mapper de manière triviale les clés sur des compartiments distincts. Un tableau semble donc un meilleur choix de structure de données qu'une table de hachage. Néanmoins, les inefficacités ne grandissent pas avec la taille de la table.
(Vous pouvez toujours utiliser une table de hachage parce que vous ne croyez pas que les ints restent plus petits que la taille de la table au fur et à mesure que le programme évolue, vous voulez que le code soit potentiellement réutilisable lorsque cette relation ne tient pas, ou vous ne le faites pas. veulent que les personnes qui lisent/maintiennent le code aient à gaspiller leur effort mental à comprendre et à maintenir la relation).
B. Vous devez calculer un hachage de la valeur. Dans cette situation, l'ordre est O(n) pour la taille des données recherchées. La recherche pourrait être O(1) après que vous ayez fait O(n), mais cela reste quand même à O(n) à mes yeux.
Nous devons faire la distinction entre la taille de la clé (par exemple, en octets) et la taille du nombre de clés stockées dans la table de hachage. Les revendications selon lesquelles les tables de hachage fournissent O(1) opérations signifient que les opérations (insertion/effacement/recherche) n'ont pas tendance à ralentir davantage lorsque le nombre de clés augmente , il passe de centaines à des milliers, voire des millions, voire des milliards (au moins si toutes les données sont accédées/mises à jour de manière aussi rapide, soit RAM ou de disque peuvent entrer en jeu, mais même le coût d'un cache raté dans le cas le plus défavorable a tendance à être un multiple constant du résultat optimal.
Pensez à un annuaire téléphonique: vous y trouverez peut-être des noms assez longs, mais que l'annuaire compte 100 noms ou 10 millions, la longueur moyenne des noms sera relativement cohérente, et le pire des cas dans l'histoire ...
Adolph Charles David Earl Frederick Gerald Hubert Irvin John Kenneth Lloyd Martin Nero Oliver Quincy Randman Sherman Sherman Thomas Uncas Victor William Xerxes Yancy Wolfeschlegelsteinhausenbergerdorff, Senior
...wc
me dit que c'est 215 caractères - ce n'est pas un limite dure supérieure à la longueur de la clé, mais nous n'avons pas à nous inquiéter de l'existence de massivement plus.
Cela vaut pour la plupart des tables de hachage du monde réel: la longueur moyenne des clés n'a pas tendance à augmenter avec le nombre de clés utilisées. Il existe des exceptions, par exemple, une routine de création de clé peut renvoyer des chaînes incorporant des entiers incrémentants, mais même à chaque fois que vous augmentez le nombre de clés d'un ordre de grandeur, vous augmentez uniquement la longueur de la clé de 1 caractère: ce n'est pas significatif.
Il est également possible de créer un hachage à partir d'une quantité de données clés de taille fixe. Par exemple, Visual C++ de Microsoft est livré avec une implémentation de bibliothèque standard de _std::hash<std::string>
_ qui crée un hachage incorporant seulement dix octets et répartis uniformément sur la chaîne. Ainsi, si les chaînes ne varient que sur d’autres index, vous obtenez des collisions (et donc, en pratique, non O(1) comportements du côté de la recherche après une collision), mais le temps nécessaire à la création du hachage a une limite supérieure stricte.
Et sauf si vous avez un hachage parfait ou une grande table de hachage, il y a probablement plusieurs éléments par seau. Donc, il y a quand même une petite recherche linéaire.
Généralement vrai, mais l’atout majeur des tables de hachage est que le nombre de clés visitées au cours de ces "petites recherches linéaires" est - pour l’approche chaînée des collisions - une fonction de la table de hachage facteur de charge (rapport entre les clés et les compartiments).
Par exemple, avec un facteur de charge de 1,0, la longueur de ces recherches linéaires est en moyenne d’environ 1,58, quel que soit le nombre de clés (voir ma réponse ici ). Pour (), le hachage fermé est un peu plus compliqué, mais pas plus grave lorsque le facteur de charge n'est pas trop élevé.
C'est techniquement vrai parce que la fonction de hachage n'est pas obligée d'utiliser toutes les informations de la clé et peut donc être à temps constant, et parce qu'un tableau assez grand peut réduire les collisions à un temps presque constant.
Ce genre de passe à côté du sujet. Tout type de structure de données associative doit en fin de compte effectuer des opérations sur chaque partie de la clé (l’inégalité peut parfois être déterminée à partir d’une partie de la clé, mais l’égalité exige généralement que chaque bit soit pris en compte). Au minimum, il peut hacher la clé une fois et stocker la valeur de hachage, et s'il utilise une fonction de hachage suffisamment forte - par ex. MD5 64 bits - il est pratiquement impossible d'ignorer la possibilité d'un hachage de deux clés ayant la même valeur (une entreprise pour laquelle j'ai travaillé a fait exactement ce qui précède pour la base de données distribuée: le temps de génération de hachage était encore insignifiant comparé aux transmissions sur le réseau WAN). Le coût de traitement de la clé n’est donc pas trop obsédant: c’est inhérent au stockage des clés, quelle que soit la structure de données, et comme dit plus haut, la tendance ne s’aggrave généralement pas avec l’augmentation du nombre de clés.
En ce qui concerne les tables de hachage assez grandes qui réduisent les collisions, cela manque également la cible. Pour un chaînage séparé, vous avez toujours une longueur de chaîne de collision moyenne constante pour tout facteur de charge donné. Elle est simplement supérieure lorsque le facteur de charge est supérieur et cette relation est non linéaire. L'utilisateur SO Hans commente ma réponse est également liée ci-dessus que:
la longueur moyenne du godet conditionnée par des godets non vides constitue une meilleure mesure de l'efficacité. C'est un/(1-e ^ {- a}) [où a est le facteur de charge, e est 2.71828 ...]
Ainsi, le facteur de charge seul détermine le nombre moyen de clés en collision que vous devez rechercher lors des opérations d'insertion/effacement/recherche. Pour un chaînage séparé, il ne suffit pas que le facteur de charge soit faible: il s'agit toujours toujours constant. Pour un adressage ouvert, même si votre revendication a une certaine validité: certains éléments en collision sont redirigés vers d'autres compartiments et peuvent alors interférer avec les opérations sur d'autres clés.
C’est vrai dans la pratique, car avec le temps, cela fonctionne, du moment que la fonction de hachage et la taille de la table sont choisies pour minimiser les collisions, même si cela implique souvent de ne pas utiliser de fonction de hachage à temps constant.
La taille de la table devrait donner un facteur de charge raisonnable compte tenu du choix entre hachage rapproché ou chaînage séparé, mais également si la fonction de hachage est un peu faible et que les clés ne sont pas très aléatoires, le fait d’avoir un nombre premier de compartiments permet souvent de réduire aussi les collisions (_hash-value % table-size
_ alors, de sorte que les modifications apportées uniquement à un bit de poids fort dans la valeur de hachage soient toujours résolues en compartiments répartis de manière pseudo-aléatoire sur différentes parties de la table de hachage).
Il existe deux paramètres sous lesquels vous pouvez obtenir les pires cas O(1).
Copié de ici
Il semble basé sur la discussion ici, que si X est le plafond de (# d'éléments dans le tableau/# de bacs), alors une meilleure réponse est O(log(X)) en supposant une implémentation efficace de la recherche de bacs.
TL; DR: Les tables de hachage garantissent O(1)
le délai prévisible si vous choisissez votre fonction de hachage uniformément et de manière aléatoire dans une famille universelle de fonctions de hachage. Le pire cas attendu n’est pas le même que le cas moyen.
_ {Disclaimer: Je ne prouve pas officiellement que les tables de hachage sont O(1)
, pour cela, jetez un coup d'œil à cette vidéo de coursera [ 1 ]. Je ne discute pas non plus des aspects amortis des tables de hachage. Ceci est orthogonal à la discussion sur le hachage et les collisions.
Je constate une confusion surprenante autour de ce sujet dans d’autres réponses et commentaires, et je tenterai d’en rectifier certains dans cette réponse longue.
Il existe différents types d'analyses des pires cas. L’analyse que la plupart des réponses ont faite ici jusqu’à présent n’est pas le pire cas, mais plutôt le cas moyen [ 2 ]. _ {Cas moyen} _ l'analyse tend à être plus pratique. Peut-être que votre algorithme a une mauvaise entrée dans le pire des cas, mais qu'il fonctionne bien pour toutes les autres entrées possibles. Bottomline est votre runtime dépend de l'ensemble de données sur lequel vous exécutez.
Considérons le pseudocode suivant de la méthode get
d'une table de hachage. Je suppose ici que nous gérons les collisions par chaînage, de sorte que chaque entrée de la table est une liste chaînée de paires (key,value)
. Nous supposons également que le nombre de compartiments m
est fixe, mais est O(n)
, où n
est le nombre d'éléments de l'entrée.
function get(a: Table with m buckets, k: Key being looked up)
bucket <- compute hash(k) modulo m
for each (key,value) in a[bucket]
return value if k == key
return not_found
Comme d'autres réponses l'ont souligné, cela fonctionne en moyenne O(1)
et dans le cas le plus défavorable O(n)
. Nous pouvons faire un petit croquis d'une preuve par défi ici. Le défi est le suivant:
(1) Vous donnez votre algorithme de table de hachage à un adversaire.
(2) L’adversaire peut l’étudier et se préparer aussi longtemps qu’il le souhaite.
(3) Enfin, l’adversaire vous donne une entrée de taille n
à insérer dans votre tableau.
La question est: à quelle vitesse votre table de hachage sur l'entrée de l'adversaire?
A partir de l'étape (1), l'adversaire connaît votre fonction de hachage. au cours de l'étape (2), l'adversaire peut créer une liste de n
éléments avec le même hash modulo m
, par ex. calculer au hasard le hachage d'un tas d'éléments; et puis dans (3) ils peuvent vous donner cette liste. Mais bon, vu que tous les éléments n
se jettent dans le même compartiment, votre algorithme prendra O(n)
temps pour parcourir la liste liée dans ce compartiment. Peu importe combien de fois nous relançons le défi, l'adversaire gagne toujours, et c'est à quel point votre algorithme est mauvais, dans le pire des cas, O(n)
.
Le défi précédent était que l’adversaire connaissait très bien notre fonction de hachage et qu’il pouvait utiliser cette connaissance pour créer la pire entrée possible… .. Si au lieu de toujours utiliser une fonction de hachage fixe, nous avions un ensemble des fonctions de hachage, H
, que l'algorithme peut choisir de manière aléatoire au moment de l'exécution? Si vous êtes curieux, H
est appelé une famille universelle de fonctions de hachage} [ 3 ]. Très bien, essayons d'ajouter quelques randomness à cela.
Supposons d’abord que notre table de hachage comporte également une graine r
, et que r
se voit attribuer un nombre aléatoire au moment de la construction. Nous l'attribuons une fois, puis il est corrigé pour cette instance de table de hachage. Revenons maintenant à notre pseudocode.
function get(a: Table with m buckets and seed r, k: Key being looked up)
rHash <- H[r]
bucket <- compute rHash(k) modulo m
for each (key,value) in a[bucket]
return value if k == key
return not_found
Si nous relevons le défi une fois de plus: à partir de l'étape (1), l'adversaire peut connaître toutes les fonctions de hachage dont nous disposons dans H
, mais la fonction de hachage spécifique que nous utilisons dépend de r
. La valeur de r
est une propriété privée de notre structure. L'adversaire ne peut pas l'inspecter à l'exécution ni la prédire à l'avance. Il ne peut donc pas créer une liste qui est toujours mauvaise pour nous. Supposons qu'à l'étape (2), l'adversaire choisisse une fonction hash
dans H
au hasard, il crée ensuite une liste de n
collisions sous hash modulo m
et l'envoie à l'étape (3). , croisant les doigts qu’au moment de l'exécution, H[r]
sera le même hash
qu’ils ont choisi.
C’est un pari sérieux pour l’adversaire, la liste qu’il a construite se confond sous hash
, mais ne sera qu’une entrée aléatoire sous toute autre fonction de hachage dans H
. S'il gagne ce pari, notre temps d'exécution sera le pire des cas, O(n)
comme avant, mais s'il perd, alors nous recevons une entrée aléatoire qui prend le temps O(1)
moyen. Et en effet, la plupart du temps, l’adversaire perdra, il ne gagne qu’une fois tous les défis de |H|
et nous pouvons faire que |H|
soit très volumineux.
Comparez ce résultat à l'algorithme précédent où l'adversaire a toujours remporté le défi. Nous passons un peu de temps ici, mais puisque la plupart du temps, l'adversaire échouera et que cela est vrai pour toutes les stratégies possibles que l'adversaire peut essayer, il s'ensuit que, même si le cas le plus défavorable est O(n)
, le cas le plus défavorable attendu est en fait O(1)
.Encore une fois, ce n'est pas une preuve formelle. La garantie que nous obtenons de cette analyse du pire scénario attendu est que notre temps d'exécution est maintenant indépendant de toute entrée spécifique. Il s’agit d’une garantie véritablement aléatoire, par opposition à l’analyse de cas moyenne dans laquelle nous avons montré qu’un adversaire motivé pouvait facilement fabriquer de mauvais intrants.
Again, this is not a formal proof. The guarantee we get from this expected worst case analysis is that our run time is now independent of any specific input. This is a truly random guarantee, as opposed to the average case analysis where we showed a motivated adversary could easily craft bad inputs.