TL; DR: garantie des tables de hachage O(1)
pire des cas si vous choisissez votre fonction de hachage uniformément au hasard 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 formellement que les tables de hachage le sont O(1)
, pour cela jetez un œil à cette vidéo de coursera [ 1 ]. Je ne parle pas non plus de l' amorti aspects des tables de hachage. C'est orthogonal à la discussion sur le hachage et les collisions.
Je vois une confusion étonnamment grande autour de ce sujet dans d'autres réponses et commentaires, et j'essaierai de rectifier certaines d'entre elles dans cette longue réponse.
Raisonner le pire des cas
Il existe différents types d'analyse des pires cas. L'analyse que la plupart des réponses ont faite jusqu'ici n'est pas le pire des cas, mais plutôt le cas moyen [ 2 ]. L' analyse de cas moyenne a tendance à être plus pratique. Peut-être que votre algorithme a une mauvaise entrée du pire des cas, mais fonctionne bien pour toutes les autres entrées possibles. En bout de ligne, votre exécution dépend de l'ensemble de données sur lequel vous exécutez.
Considérez le pseudocode suivant de la get
méthode d'une table de hachage. Ici, je suppose que nous gérons la collision par chaînage, donc chaque entrée de la table est une liste chaînée de (key,value)
paires. Nous supposons également que le nombre de compartiments m
est fixe mais l'est O(n)
, où n
est le nombre d'éléments dans 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 dans la moyenne O(1)
et dans le pire des cas 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 table.
La question est: à quelle vitesse votre table de hachage est-elle sur l'entrée de l'adversaire?
À partir de l'étape (1), l'adversaire connaît votre fonction de hachage; lors de l'étape (2), l'adversaire peut élaborer une liste d' n
éléments avec celui-ci hash modulo m
, par exemple en calculant de manière aléatoire le hachage d'un groupe d'éléments; puis dans (3) ils peuvent vous donner cette liste. Mais voilà, puisque tous les n
éléments sont hachés dans le même compartiment, votre algorithme prendra du O(n)
temps pour parcourir la liste liée dans ce compartiment. Peu importe le nombre de fois que 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)
.
Comment se fait-il que le hachage soit O (1)?
Ce qui nous a déconcertés dans le défi précédent, c'est que l'adversaire connaissait très bien notre fonction de hachage et pouvait utiliser ces connaissances pour créer la pire entrée possible. Et si au lieu de toujours utiliser une fonction de hachage fixe, nous avions en fait un ensemble de fonctions de hachage H
, que l'algorithme pouvait choisir au hasard au moment de l'exécution? Au cas où vous êtes curieux, H
on appelle cela une famille universelle de fonctions de hachage [ 3 ]. Très bien, essayons d'ajouter un peu de hasard à cela.
Supposons d'abord que notre table de hachage comprenne également une graine r
et r
soit affectée à 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 essayons le défi une fois de plus: à partir de l'étape (1), l'adversaire peut connaître toutes les fonctions de hachage que nous avons H
, mais maintenant la fonction de hachage spécifique que nous utilisons dépend r
. La valeur de r
est privée pour notre structure, l'adversaire ne peut pas l'inspecter au moment de l'exécution, ni la prédire à l'avance, donc il ne peut pas concocter une liste qui est toujours mauvaise pour nous. Supposons que l' étape (2) l'adversaire choisit une fonction hash
dans H
au hasard, il artisanat alors une liste des n
collisions sous hash modulo m
et envoie que pour l' étape (3), qui croise les doigts lors de l' exécution H[r]
seront les mêmes hash
qu'ils ont choisi.
C'est un pari sérieux pour l'adversaire, la liste qu'il a créée se heurte 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 juste une entrée aléatoire qui prend le O(1)
temps moyen . Et en effet la plupart du temps l'adversaire perdra, il ne remportera qu'une seule fois tous les |H|
défis, et nous pouvons faire |H|
être très gros.
Comparez ce résultat à l'algorithme précédent où l'adversaire a toujours remporté le défi. Agitant un peu la main ici, mais comme la plupart du temps l'adversaire échouera, et cela est vrai pour toutes les stratégies possibles que l'adversaire peut essayer, il s'ensuit que bien que le pire des cas soit O(n)
, le pire des cas attendus 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 cas attendu est que notre temps d'exécution est désormais indépendant de toute entrée spécifique . Il s'agit d'une garantie vraiment aléatoire, contrairement à l'analyse de cas moyenne où nous avons montré qu'un adversaire motivé pouvait facilement créer de mauvaises entrées.