Lorsqu’on donne un ensemble statique d’objets (statique dans le sens où une fois chargé, il modifie rarement, voire jamais) dans lequel des recherches simultanées répétées sont nécessaires avec des performances optimales, lequel est préférable, un HashMap
ou un tableau avec une recherche binaire utilisant un comparateur personnalisé?
La réponse est-elle une fonction de type objet ou struct? Fonctions de hachage et/ou égales? Hachage unique? Taille de la liste? Hashset
taille/taille définie?
La taille de l'ensemble que je regarde peut être comprise entre 500 000 et 10 m - au cas où cette information serait utile.
Pendant que je cherche une réponse en C #, je pense que la vraie réponse mathématique ne réside pas dans le langage, je n'inclus donc pas cette balise. Cependant, s'il y a des choses spécifiques à C # dont il faut être conscient, cette information est souhaitée.
Ok, je vais essayer d'être bref.
C # réponse courte:
Testez les deux approches différentes.
.NET vous donne les outils pour changer votre approche avec une ligne de code ..__ Sinon, utilisez System.Collections.Generic.Dictionary et assurez-vous de l'initialiser avec un grand nombre en tant que capacité initiale ou vous passerez le reste de votre la vie insérant des éléments en raison du travail que GC doit faire pour collecter les anciens tableaux de compartiments.
Réponse plus longue:
Une table de hachage a presque des temps de recherche constants et obtenir un élément dans une table de hachage dans le monde réel ne nécessite pas seulement de calculer un hachage.
Pour obtenir un élément, votre hashtable fera quelque chose comme ceci:
Les temps de consultation dépendent de la qualité de votre sortie (faible) et de la rapidité de votre fonction de hachage, du nombre de compartiments que vous utilisez et de la rapidité du comparateur de clés. Ce n'est pas toujours la meilleure solution.
Une explication meilleure et plus profonde: http://en.wikipedia.org/wiki/Hash_table
Pour les très petites collections, la différence sera négligeable. Au bas de votre gamme (500 000 éléments), vous constaterez une différence si vous effectuez de nombreuses recherches. Une recherche binaire va être O (log n), alors qu'une recherche de hachage sera O (1), amortie . Ce n'est pas la même chose que vraiment constant, mais il faudrait quand même avoir une fonction de hachage plutôt terrible pour obtenir de meilleures performances qu'une recherche binaire.
(Quand je dis "terrible hash", je veux dire quelque chose comme:
hashCode()
{
return 0;
}
Ouais, il flambe vite, mais votre carte de hachage devient une liste chaînée.)
ialiashkevich a écrit du code C # en utilisant un tableau et un dictionnaire pour comparer les deux méthodes, mais en utilisant des valeurs longues pour les clés. Je voulais tester quelque chose qui exécuterait une fonction de hachage lors de la recherche, alors j'ai modifié ce code. Je l'ai modifié pour utiliser des valeurs String et j'ai refactoré les sections de remplissage et de recherche dans leurs propres méthodes afin de faciliter la visualisation dans un profileur. J'ai également laissé dans le code qui utilisait les valeurs Long, juste comme point de comparaison. Enfin, je me suis débarrassé de la fonction de recherche binaire personnalisée et ai utilisé celle de la classe Array
.
Voici ce code:
class Program
{
private const long capacity = 10_000_000;
private static void Main(string[] args)
{
testLongValues();
Console.WriteLine();
testStringValues();
Console.ReadLine();
}
private static void testStringValues()
{
Dictionary<String, String> dict = new Dictionary<String, String>();
String[] arr = new String[capacity];
Stopwatch stopwatch = new Stopwatch();
Console.WriteLine("" + capacity + " String values...");
stopwatch.Start();
populateStringArray(arr);
stopwatch.Stop();
Console.WriteLine("Populate String Array: " + stopwatch.ElapsedMilliseconds);
stopwatch.Reset();
stopwatch.Start();
populateStringDictionary(dict, arr);
stopwatch.Stop();
Console.WriteLine("Populate String Dictionary: " + stopwatch.ElapsedMilliseconds);
stopwatch.Reset();
stopwatch.Start();
Array.Sort(arr);
stopwatch.Stop();
Console.WriteLine("Sort String Array: " + stopwatch.ElapsedMilliseconds);
stopwatch.Reset();
stopwatch.Start();
searchStringDictionary(dict, arr);
stopwatch.Stop();
Console.WriteLine("Search String Dictionary: " + stopwatch.ElapsedMilliseconds);
stopwatch.Reset();
stopwatch.Start();
searchStringArray(arr);
stopwatch.Stop();
Console.WriteLine("Search String Array: " + stopwatch.ElapsedMilliseconds);
}
/* Populate an array with random values. */
private static void populateStringArray(String[] arr)
{
for (long i = 0; i < capacity; i++)
{
arr[i] = generateRandomString(20) + i; // concatenate i to guarantee uniqueness
}
}
/* Populate a dictionary with values from an array. */
private static void populateStringDictionary(Dictionary<String, String> dict, String[] arr)
{
for (long i = 0; i < capacity; i++)
{
dict.Add(arr[i], arr[i]);
}
}
/* Search a Dictionary for each value in an array. */
private static void searchStringDictionary(Dictionary<String, String> dict, String[] arr)
{
for (long i = 0; i < capacity; i++)
{
String value = dict[arr[i]];
}
}
/* Do a binary search for each value in an array. */
private static void searchStringArray(String[] arr)
{
for (long i = 0; i < capacity; i++)
{
int index = Array.BinarySearch(arr, arr[i]);
}
}
private static void testLongValues()
{
Dictionary<long, long> dict = new Dictionary<long, long>(Int16.MaxValue);
long[] arr = new long[capacity];
Stopwatch stopwatch = new Stopwatch();
Console.WriteLine("" + capacity + " Long values...");
stopwatch.Start();
populateLongDictionary(dict);
stopwatch.Stop();
Console.WriteLine("Populate Long Dictionary: " + stopwatch.ElapsedMilliseconds);
stopwatch.Reset();
stopwatch.Start();
populateLongArray(arr);
stopwatch.Stop();
Console.WriteLine("Populate Long Array: " + stopwatch.ElapsedMilliseconds);
stopwatch.Reset();
stopwatch.Start();
searchLongDictionary(dict);
stopwatch.Stop();
Console.WriteLine("Search Long Dictionary: " + stopwatch.ElapsedMilliseconds);
stopwatch.Reset();
stopwatch.Start();
searchLongArray(arr);
stopwatch.Stop();
Console.WriteLine("Search Long Array: " + stopwatch.ElapsedMilliseconds);
}
/* Populate an array with long values. */
private static void populateLongArray(long[] arr)
{
for (long i = 0; i < capacity; i++)
{
arr[i] = i;
}
}
/* Populate a dictionary with long key/value pairs. */
private static void populateLongDictionary(Dictionary<long, long> dict)
{
for (long i = 0; i < capacity; i++)
{
dict.Add(i, i);
}
}
/* Search a Dictionary for each value in a range. */
private static void searchLongDictionary(Dictionary<long, long> dict)
{
for (long i = 0; i < capacity; i++)
{
long value = dict[i];
}
}
/* Do a binary search for each value in an array. */
private static void searchLongArray(long[] arr)
{
for (long i = 0; i < capacity; i++)
{
int index = Array.BinarySearch(arr, arr[i]);
}
}
/**
* Generate a random string of a given length.
* Implementation from https://stackoverflow.com/a/1344258/1288
*/
private static String generateRandomString(int length)
{
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var stringChars = new char[length];
var random = new Random();
for (int i = 0; i < stringChars.Length; i++)
{
stringChars[i] = chars[random.Next(chars.Length)];
}
return new String(stringChars);
}
}
Voici les résultats avec différentes tailles de collections. (Les temps sont en millisecondes.)
500000 valeurs longues ...
Remplir un dictionnaire long: 26
Remplir un tableau long: 2
Rechercher dictionnaire long: 9
Recherche tableau long: 80500000 valeurs de chaîne ...
Remplir un tableau de chaînes: 1237
Remplir Dictionnaire De Chaîne: 46
Tableau de classement: 1755
Dictionnaire de recherche: 27
Cordes de recherche: 15691000000 Longue valeurs ...
Remplir un dictionnaire long: 58
Remplir un tableau long: 5
Rechercher dictionnaire long: 23
Recherche tableau long: 1361000000 String valeurs ...
Remplir un tableau de chaînes: 2070
Remplir Dictionnaire De Chaîne: 121
Tableau de classement: 3579
Dictionnaire de recherche: 58
Cordes de recherche: 32673000000 Longues valeurs ...
Remplir un dictionnaire long: 207
Remplir un tableau long: 14
Rechercher dictionnaire long: 75
Recherche tableau long: 4353000000 valeurs de chaîne ...
Remplir un tableau de chaînes: 5553
Remplir Dictionnaire De Chaîne: 449
Tableau de classement: 11695
Dictionnaire de recherche: 194
Cordes de recherche: 1059410000000 Longues valeurs ...
Remplir un dictionnaire long: 521
Remplir un tableau long: 47
Rechercher dictionnaire long: 202
Recherche tableau long: 118110000000 Valeurs de chaîne ...
Remplir un tableau de chaînes: 18119
Remplir Dictionnaire De Chaîne: 1088
Tableau de classement: 28174
Dictionnaire de recherche: 747
Chaîne de recherche: 26503
Et à titre de comparaison, voici la sortie du profileur de la dernière exécution du programme (10 millions d’enregistrements et de recherches). J'ai mis en évidence les fonctions pertinentes. Ils sont assez en accord avec les métriques de chronométrage Stopwatch ci-dessus.
Vous pouvez constater que les recherches dans le dictionnaire sont beaucoup plus rapides que la recherche binaire et que, comme on pouvait s'y attendre, la différence est d'autant plus marquée que la collection est volumineuse. Donc, si vous avez une fonction de hachage raisonnable (assez rapide avec peu de collisions), une recherche de hachage devrait battre la recherche binaire des collections dans cette plage.
Les réponses de Bobby, Bill et Corbin sont fausses. O(1) n'est pas plus lent que O (log n) pour un n fixé/borné:
log (n) est constant, donc cela dépend du temps constant.
Et pour une fonction de hachage lente, avez-vous déjà entendu parler de md5?
L'algorithme de hachage de chaîne par défaut touche probablement tous les caractères et peut être facilement 100 fois plus lent que la comparaison moyenne pour les clés de chaîne longue. Été là, fait ça.
Vous pourriez peut-être (partiellement) utiliser une base. Si vous pouvez diviser en 256 blocs approximativement de la même taille, vous envisagez une recherche binaire de 2k à 40k. Cela est susceptible d’offrir de bien meilleures performances.
[Modifier] Trop de gens votent pour ce qu'ils ne comprennent pas.
Les comparaisons de chaînes pour la recherche binaire Les ensembles triés ont une propriété très intéressante: plus ils se rapprochent de la cible, plus ils ralentissent. Ils commenceront par casser le premier caractère, à la fin le dernier. Assumer un temps constant pour eux est incorrect.
La seule réponse raisonnable à cette question est: cela dépend. Cela dépend de la taille de vos données, de leur forme, de votre implémentation de hachage, de votre implémentation de la recherche binaire et de l'emplacement de vos données (même si cela n'est pas mentionné dans la question). Quelques autres réponses en disent autant, je peux donc simplement supprimer ceci. Cependant, il serait peut-être agréable de partager ce que j’ai appris des commentaires avec ma réponse initiale.
Étant donné les commentaires, vous pouvez supposer que les personnes qui utilisent des tables de hachage sont dérangées. Les tables de hachage sont-elles imprudentes et dangereuses? Est-ce que ces gens sont fous?
Il s'avère qu'ils ne le sont pas. Tout comme les arbres binaires sont bons à certains égards (traversée de données dans l’ordre, efficacité du stockage), les tables de hachage ont également leur temps de briller. En particulier, ils peuvent très bien réduire le nombre de lectures nécessaires pour récupérer vos données. Un algorithme de hachage peut générer un emplacement et y accéder directement en mémoire ou sur disque, tandis que la recherche binaire lit les données lors de chaque comparaison pour décider de la lecture suivante. Chaque lecture a le potentiel d’un cache cache qui est un ordre de grandeur (ou plus) plus lent que celui d’une instruction de la CPU.
Cela ne veut pas dire que les tables de hachage sont meilleures que la recherche binaire. Ils ne sont pas. Cela ne veut pas dire non plus que toutes les implémentations de recherche par hachage et binaire sont les mêmes. Ils ne sont pas. Si j'ai un point, c'est ceci: les deux approches existent pour une raison. C'est à vous de décider lequel vous convient le mieux.
Réponse originale:
Les algorithmes de hachage sont O(1) alors que la recherche binaire est O (log n). Donc, comme n se rapproche de l'infini, les performances de hachage s'améliorent par rapport aux valeurs binaires chercher. Votre kilométrage variera en fonction de n, votre hash mise en œuvre, et votre implémentation de recherche binaire.
Discussion intéressante sur O(1) . Paraphrasé:
O (1) ne veut pas dire instantané. Cela signifie que la performance ne le fait pas changer à mesure que la taille de n augmente. Vous pouvez concevoir un algorithme de hachage c'est tellement lent que personne ne l'utilisera jamais et ce serait toujours O (1) . Je suis à peu près sûr que .NET/C # ne souffre pas d'un hachage d'un coût prohibitif, toutefois ;)
Surpris, personne n'a mentionné le hachage de coucous, qui fournit une garantie O(1) et, contrairement au hachage parfait, est capable d'utiliser toute la mémoire qu'il alloue, où un hachage parfait peut aboutir à une garantie O(1), mais gaspillant la plus grande partie de son allocation. La mise en garde? Le temps d'insertion peut être très lent, en particulier lorsque le nombre d'éléments augmente, car toute l'optimisation est effectuée pendant la phase d'insertion.
Je crois qu'une version de ceci est utilisée dans le matériel de routeur pour les recherches d'ip.
Voir texte du lien
Les hachages sont généralement plus rapides, bien que les recherches binaires présentent de meilleures caractéristiques dans le pire des cas. Un accès par hachage est généralement un calcul visant à obtenir une valeur de hachage pour déterminer le "compartiment" dans lequel un enregistrement sera placé. La performance dépend donc généralement de la manière dont les enregistrements sont distribués et de la méthode utilisée pour rechercher le compartiment. Une mauvaise fonction de hachage (laissant quelques compartiments avec une multitude d'enregistrements) avec une recherche linéaire dans les compartiments entraînera une recherche lente. (Sur la troisième main, si vous lisez un disque plutôt que de la mémoire, les compartiments de hachage sont susceptibles d'être contigus alors que l'arborescence binaire garantit à peu près un accès non local.)
Si vous voulez généralement rapide, utilisez le hachage. Si vous voulez vraiment des performances limitées, vous pouvez utiliser l’arbre binaire.
Dictionary/Hashtable utilise plus de mémoire et prend plus de temps à remplir qu'à comparer à array . Mais la recherche est effectuée plus rapidement par Dictionary plutôt que par Binary Search au sein de array.
Voici les numéros pour 10 Million of Int64 éléments à rechercher et à renseigner ..__ Plus un exemple de code que vous pouvez exécuter par vous-même.
Mémoire de Dictionnaire: 462,836
Mémoire matricielle: 88,376
Dictionnaire de calcul: 402
Remplir un tableau: 23
Dictionnaire de recherche: 176
Matrice de recherche: 680
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace BinaryVsDictionary
{
internal class Program
{
private const long Capacity = 10000000;
private static readonly Dictionary<long, long> Dict = new Dictionary<long, long>(Int16.MaxValue);
private static readonly long[] Arr = new long[Capacity];
private static void Main(string[] args)
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for (long i = 0; i < Capacity; i++)
{
Dict.Add(i, i);
}
stopwatch.Stop();
Console.WriteLine("Populate Dictionary: " + stopwatch.ElapsedMilliseconds);
stopwatch.Reset();
stopwatch.Start();
for (long i = 0; i < Capacity; i++)
{
Arr[i] = i;
}
stopwatch.Stop();
Console.WriteLine("Populate Array: " + stopwatch.ElapsedMilliseconds);
stopwatch.Reset();
stopwatch.Start();
for (long i = 0; i < Capacity; i++)
{
long value = Dict[i];
// Console.WriteLine(value + " : " + RandomNumbers[i]);
}
stopwatch.Stop();
Console.WriteLine("Search Dictionary: " + stopwatch.ElapsedMilliseconds);
stopwatch.Reset();
stopwatch.Start();
for (long i = 0; i < Capacity; i++)
{
long value = BinarySearch(Arr, 0, Capacity, i);
// Console.WriteLine(value + " : " + RandomNumbers[i]);
}
stopwatch.Stop();
Console.WriteLine("Search Array: " + stopwatch.ElapsedMilliseconds);
Console.ReadLine();
}
private static long BinarySearch(long[] arr, long low, long hi, long value)
{
while (low <= hi)
{
long median = low + ((hi - low) >> 1);
if (arr[median] == value)
{
return median;
}
if (arr[median] < value)
{
low = median + 1;
}
else
{
hi = median - 1;
}
}
return ~low;
}
}
}
Je soupçonne fortement que dans un ensemble de problèmes de taille ~ 1M, le hachage serait plus rapide.
Juste pour les chiffres:
une recherche binaire nécessiterait environ 20 comparaisons (2 ^ 20 == 1M)
une recherche de hachage nécessiterait 1 calcul de hachage sur la clé de recherche et éventuellement une poignée de comparaisons pour résoudre d'éventuelles collisions
Edit: les chiffres:
for (int i = 0; i < 1000 * 1000; i++) {
c.GetHashCode();
}
for (int i = 0; i < 1000 * 1000; i++) {
for (int j = 0; j < 20; j++)
c.CompareTo(d);
}
fois: c = "abcde", d = "rwerij" hashcode: 0.0012 secondes. Comparer: 2,4 secondes.
disclaimer: En fait, comparer une recherche de hachage à une recherche binaire pourrait être meilleur que ce test pas tout à fait pertinent. Je ne suis même pas sûr que GetHashCode soit mémorisé sous le capot
Je dirais que cela dépend principalement de la performance du hachage et des méthodes de comparaison. Par exemple, lorsque vous utilisez des clés de chaîne très longues mais aléatoires, une comparaison produira toujours un résultat très rapide, mais une fonction de hachage par défaut traitera la chaîne entière.
Mais dans la plupart des cas, la carte de hachage devrait être plus rapide.
Je me demande pourquoi personne n'a mentionné hachage parfait .
Cela n'a de sens que si votre jeu de données est fixé pour une longue période, mais ce qu'il fait analyse les données et construit une fonction de hachage parfaite qui garantit l'absence de collisions.
Plutôt chouette, si votre ensemble de données est constant et que le temps nécessaire au calcul de la fonction est petit comparé au temps d'exécution de l'application.
Ici il est décrit comment les hachages sont construits et parce que l'univers des clés est relativement grand et que les fonctions de hachage sont conçues pour être "très injectives", de sorte que les collisions se produisent rarement. Le temps d'accès d'une table de hachage n'est pas O(1) en fait ... c'est quelque chose basé sur certaines probabilités ..__ Mais, il est raisonnable de dire que le temps d'accès d'un hash est presque toujours inférieur au temps O (log_2 (n))
Cela dépend de la façon dont vous gérez les doublons pour les tables de hachage (le cas échéant). Si vous souhaitez autoriser les doublons de hachage (aucune fonction de hachage n'est parfaite), il reste O(1) pour la recherche de clé primaire, mais la recherche de la "bonne" valeur peut être coûteuse. La réponse est alors, théoriquement la plupart du temps, les hashes sont plus rapides. YMMV en fonction des données que vous mettez ici ...
Cette question est plus compliquée que la portée des performances d'un algorithme pur. Si nous supprimons les facteurs qui font que l'algorithme de recherche binaire est plus compatible avec le cache, la recherche de hachage est plus rapide au sens général. Le meilleur moyen de comprendre est de créer un programme et de désactiver les options d'optimisation du compilateur. Nous pourrions constater que la recherche de hachage est plus rapide étant donné que l'efficacité de son algorithme est de O(1) au sens général.
Toutefois, lorsque vous activez l'optimisation du compilateur et tentez le même test avec un nombre d'échantillons inférieur à moins de 10 000, la recherche binaire a surperformé la recherche par hachage en tirant parti des avantages de sa structure de données conviviale pour le cache.
Bien entendu, le hachage est le plus rapide pour un ensemble de données aussi volumineux.
Une façon d'accélérer encore plus les choses, car les données changent rarement, consiste à générer par code un code ad-hoc pour effectuer la première couche de recherche sous la forme d'une instruction de commutateur géant (si votre compilateur peut le gérer), puis vous lancer le seau résultant.
La réponse dépend. Pensons que le nombre d'éléments 'n' est très grand. Si vous savez bien écrire une meilleure fonction de hachage qui entraîne moins de collisions, le hachage est le meilleur .Notez que La fonction de hachage est exécutée une seule fois à la recherche et est dirigée vers le compartiment correspondant. Donc, si n est élevé, les frais généraux ne sont pas élevés.
Problème dans Hashtable: Mais le problème dans les tables de hachage est que si la fonction de hachage n'est pas bonne (plus de collisions se produisent), la recherche n'est pas O (1). Il a tendance à O(n) car la recherche dans un compartiment est une recherche linéaire. Peut être pire qu'un arbre binaire . problème dans l'arborescence binaire: Dans l'arborescence binaire, si l'arborescence n'est pas équilibrée, elle a aussi tendance à O (n). Par exemple, si vous avez inséré 1,2,3,4,5 dans un arbre binaire, cela serait probablement une liste . Donc, Si vous pouvez voir une bonne méthodologie de hachage, utilisez une table de hachage Sinon, vous feriez mieux d'utiliser un arbre binaire.
Ceci est davantage un commentaire sur la réponse de Bill, qui a eu beaucoup de votes positifs, même si c'est faux. J'ai donc dû poster ceci.
Je vois beaucoup de discussions sur ce qui constitue la pire complexité d’une recherche dans une table de hachage, et sur ce qui est considéré comme une analyse amortie/ce qui n’est pas ..___. Veuillez vérifier le lien ci-dessous.
Complexité d'exécution de la table de hachage (insertion, recherche et suppression)
dans le pire des cas, la complexité est O(n) et non O(1), contrairement à ce que dit Bill. Et donc sa complexité O(1) n’est pas amortie puisque cette analyse ne peut être utilisée que dans les cas les plus graves (son propre lien wikipedia le dit aussi)