76 votes

Qu'est-ce qui est le plus rapide, la recherche par hachage ou la recherche binaire ?

Lorsque l'on dispose d'un ensemble statique d'objets (statique dans le sens où, une fois chargé, il ne change que rarement, voire jamais) dans lequel des recherches simultanées répétées sont nécessaires pour obtenir des performances optimales, quelle est la meilleure solution, un système d'archivage de type HashMap ou un tableau avec une recherche binaire utilisant un comparateur personnalisé ?

La réponse est-elle fonction du type d'objet ou de structure ? Performances des fonctions de hachage et/ou d'égalité ? Unicité du hachage ? Taille de la liste ? Hashset taille/set size ?

La taille de l'ensemble que j'envisage peut aller de 500 000 à 10 millions d'euros, au cas où cette information serait utile.

Bien que je cherche une réponse en C#, je pense que la vraie réponse mathématique ne se trouve pas dans le langage, donc je n'inclus pas cette balise. Cependant, s'il y a des choses spécifiques à C# dont il faut être conscient, cette information est souhaitée.

1 votes

Qu'est-ce que le "lookup" ? Voulez-vous seulement tester l'appartenance (si un élément particulier existe ou non) ? Ou bien avez-vous des paires clé-valeur, et voulez-vous trouver la valeur associée à une certaine clé ?

0 votes

Cela dépend du niveau de perfection de la fonction de hachage.

56voto

Bill the Lizard Points 147311

Pour les très petites collections, la différence sera négligeable. À l'extrémité inférieure de votre gamme (500 000 éléments), vous commencerez à voir une différence si vous faites beaucoup de recherches. Une recherche binaire sera O(log n), alors qu'une recherche par hachage sera O(1), amorti . Ce n'est pas la même chose que d'être vraiment constant, mais il faudrait quand même avoir une fonction de hachage assez terrible pour obtenir des performances inférieures à celles d'une recherche binaire.

(Quand je dis "hachage terrible", je veux dire quelque chose comme :

hashCode()
{
    return 0;
}

Oui, c'est très rapide en soi, mais cela fait que votre carte de hachage devient une liste liée).

ialiashkevich J'ai é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 réellement une fonction de hachage pendant la recherche, alors j'ai modifié ce code. Je l'ai changé pour utiliser des valeurs String, et j'ai refactorisé les sections populate et lookup dans leurs propres méthodes pour que ce soit plus facile à voir dans un profiler. J'ai également conservé le code qui utilisait des valeurs Long, juste comme point de comparaison. Enfin, je me suis débarrassé de la fonction de recherche binaire personnalisée et j'ai utilisé celle de la section Array classe.

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 plusieurs tailles de collections différentes. (Les temps sont en millisecondes).

500000 Valeurs longues...
Remplir le dictionnaire long : 26
Remplir le tableau long : 2
Recherche dans le dictionnaire long : 9
Recherche Long Array : 80

500000 Valeurs de chaîne...
Remplir le tableau des chaînes : 1237
Remplir le dictionnaire des chaînes : 46
Trier un tableau de chaînes de caractères : 1755
Dictionnaire des chaînes de recherche : 27
Tableau des chaînes de recherche : 1569

1000000 Valeurs longues...
Remplir le dictionnaire long : 58
Remplir le Long Array : 5
Recherche dans le dictionnaire long : 23
Recherche Long Array : 136

1000000 valeurs de chaîne...
Remplir le tableau de chaînes : 2070
Remplir le dictionnaire de chaînes : 121
Trier un tableau de chaînes de caractères : 3579
Dictionnaire des chaînes de recherche : 58
Tableau des chaînes de recherche : 3267

3000000 Valeurs longues...
Remplir le dictionnaire long : 207
Remplir le Long Array : 14
Recherche dans le dictionnaire long : 75
Recherche Long Array : 435

3000000 valeurs de chaîne...
Remplir le tableau des chaînes : 5553
Remplir le dictionnaire des chaînes : 449
Trier le tableau des chaînes de caractères : 11695
Dictionnaire des chaînes de recherche : 194
Tableau des chaînes de recherche : 10594

10000000 Valeurs longues...
Remplir le dictionnaire long : 521
Remplir le Long Array : 47
Recherche dans le dictionnaire long : 202
Recherche Long Array : 1181

10000000 Valeurs de chaîne...
Remplir le tableau des chaînes : 18119
Remplir le dictionnaire des chaînes : 1088
Trier le tableau des chaînes de caractères : 28174
Dictionnaire des chaînes de recherche : 747
Tableau des chaînes de recherche : 26503

Et à titre de comparaison, voici la sortie du profileur pour la dernière exécution du programme (10 millions d'enregistrements et de consultations). J'ai mis en évidence les fonctions pertinentes. Elles correspondent assez bien aux mesures de temps du chronomètre ci-dessus.

Profiler output for 10 million records and lookups

Vous pouvez constater que les recherches dans le dictionnaire sont beaucoup plus rapides que la recherche binaire, et (comme prévu) la différence est d'autant plus prononcée que la collection est grande. Donc, si vous disposez d'une fonction de hachage raisonnable (assez rapide avec peu de collisions), une consultation de hachage devrait battre la recherche binaire pour les collections de cette taille.

1 votes

Md5 serait totalement inapproprié comme hachage pour rechercher des valeurs dans une table de hachage. C'est un hachage cryptographique.

12 votes

Pas "totalement inapproprié", juste lent. Et même les bonnes fonctions de hachage non cryptographiques peuvent en effet être plus lentes que la recherche binaire pour les petites tailles.

1 votes

Oui, le hachage de chaîne par défaut est une fonction de hachage terrible. Si les clés sont longues, le hachage sera beaucoup plus lent que la comparaison moyenne.

40voto

Stephan Eggermont Points 11224

Les réponses de Bobby, Bill et Corbin sont fausses. O(1) n'est pas plus lent que O(log n) pour un n fixe/limité :

log(n) est constant, il dépend donc 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 longues. Je suis passé par là, j'ai fait ça.

Vous pourriez être en mesure d'utiliser (partiellement) un radix. Si vous pouvez diviser en 256 blocs de taille approximativement égale, vous pouvez envisager une recherche binaire de 2 000 à 40 000 caractères. Cela devrait permettre d'obtenir de bien meilleures performances.

[Edit] Trop de gens votent ce qu'ils ne comprennent pas.

Les comparaisons de chaînes de caractères pour la recherche binaire d'ensembles triés ont une propriété très intéressante : elles deviennent plus lentes à mesure qu'elles se rapprochent de la cible. Au début, elles s'arrêtent sur le premier caractère, à la fin seulement sur le dernier. Il est incorrect de supposer un temps constant pour ces comparaisons.

0 votes

Corbin, regarde ce que signifie la notation du grand O.

13 votes

@Stephan : Nous avons tous les trois dit que O(1) est plus rapide que O(log n). Tu dois aussi regarder ce que signifie la notation O(log n). Elle compare l'utilisation relative des ressources des algorithmes lorsque la taille de l'entrée change. Cela n'a pas de sens de parler d'un n fixe.

0 votes

La question était "lequel est le meilleur". Je suppose que cela signifie "qui prend moins de temps". Pas besoin d'ergoter sur le fait que n soit constant.

27voto

Corbin March Points 18522

La seule réponse raisonnable à cette question est : cela dépend. Cela dépend de la taille de vos données, de la forme de vos données, de votre implémentation de hachage, de votre implémentation de recherche binaire, et de l'endroit où se trouvent vos données (même si ce n'est pas mentionné dans la question). Quelques autres réponses disent la même chose, donc je pourrais simplement supprimer cette réponse. Cependant, il pourrait être intéressant de partager ce que j'ai appris des réactions à ma réponse originale.

  1. J'ai écrit, " Les algorithmes de hachage sont O(1) alors que la recherche binaire est O(log n). "Comme indiqué dans les commentaires, la notation Big O évalue la complexité, pas la vitesse. Ceci est tout à fait vrai. Il convient de noter que nous utilisons généralement la complexité pour avoir une idée des besoins en temps et en espace d'un algorithme. Ainsi, bien qu'il soit stupide de supposer que la complexité est strictement la même chose que la vitesse, estimer la complexité sans avoir le temps ou l'espace à l'esprit est inhabituel. Ma recommandation : évitez la notation Big O.
  2. J'ai écrit, " Donc, lorsque n s'approche de l'infini ..." - C'est la chose la plus stupide que j'aurais pu inclure dans une réponse. L'infini n'a rien à voir avec votre problème. Tu mentionnes une limite supérieure de 10 millions. Ignorez l'infini. Comme le soulignent les commentateurs, les très grands nombres créent toutes sortes de problèmes avec un hachage. (Les très grands nombres ne font pas non plus de la recherche binaire une promenade dans le parc.) Ma recommandation : ne mentionnez pas l'infini à moins que vous ne vouliez dire l'infini.
  3. Également dans les commentaires : attention aux hachages de chaînes de caractères par défaut (Vous hachurez des chaînes de caractères ? Vous ne le mentionnez pas.), les index de base de données sont souvent des b-trees (nourriture pour la réflexion). Ma recommandation : considérez toutes vos options. Considérez d'autres structures de données et d'autres approches... comme un bon vieux essai (pour le stockage et la récupération de chaînes de caractères) ou un fichier R-tree (pour les données spatiales) ou un MA-FSA (Minimal Acyclic Finite State Automaton - faible encombrement).

Au vu des commentaires, on pourrait penser que les personnes qui utilisent des tables de hachage sont dérangées. Les tables de hachage sont-elles imprudentes et dangereuses ? Ces personnes sont-elles folles ?

Il s'avère qu'ils ne le sont pas. Tout comme les arbres binaires sont bons pour certaines choses (traversée de données en ordre, efficacité du stockage), les tables de hachage ont aussi leur heure de gloire. En particulier, elles peuvent être très efficaces pour réduire le nombre de lectures nécessaires à l'extraction de vos données. Un algorithme de hachage peut générer un emplacement et l'atteindre directement en mémoire ou sur le disque, tandis que la recherche binaire lit les données à chaque comparaison pour décider de la lecture suivante. Chaque lecture peut entraîner une absence de cache, ce qui est un ordre de grandeur (ou plus) plus lent qu'une instruction du CPU.

Cela ne veut pas dire que les tables de hachage sont meilleures que la recherche binaire. Elles ne le sont pas. Il ne s'agit pas non plus de suggérer que toutes les implémentations de hachage et de recherche binaire sont identiques. Elles ne le sont pas. Si j'ai une idée, c'est celle-ci : les deux approches existent pour une raison. C'est à vous de décider laquelle est la plus adaptée à vos besoins.

Réponse originale :


Les algorithmes de hachage sont O(1) alors que la recherche binaire est O(log n). Ainsi, lorsque n approche de l'infini, les performances du hachage s'améliorent par rapport à la binaire. Votre kilométrage variera en fonction de n, de votre implémentation de hachage et de votre implémentation de la recherche binaire.

Discussion intéressante sur O(1) . Paraphrasé :

O(1) ne veut pas dire instantané. Cela signifie que la performance ne change pas change pas avec la croissance de la taille de n. Vous pouvez concevoir un algorithme de hachage qui est si lent que personne ne l'utilisera jamais et il sera toujours O(1). Je suis presque sûr que .NET/C# ne souffre pas d'un hachage prohibitif, cependant ;)

1 votes

Je ne sais pas pourquoi cette question a été rétrogradée - bonne réponse et point intéressant. +1.

11 votes

-1 : La notation Big O mesure la complexité, et non la vitesse par rapport à d'autres algorithmes. L'affirmation selon laquelle les hachages sont O(1) et donc plus rapides que les recherches binaires O(log n) n'est pas strictement correcte.

3 votes

Et ce n'est même pas correct d'un point de vue pratique. Les hachages de chaîne par défaut touchent la chaîne entière et peuvent être beaucoup plus lents que les comparaisons.

23voto

Maghis Points 707

Ok, je vais essayer d'être bref.

Réponse courte en C# :

Testez les deux approches différentes.

.NET vous donne les outils nécessaires pour changer votre approche en une ligne de code. Sinon, utilisez System.Collections.Generic.Dictionary et assurez-vous de l'initialiser avec un grand nombre comme capacité initiale ou vous passerez le reste de votre vie à insérer des éléments à cause du travail que GC doit faire pour collecter les vieux tableaux de seaux.

Réponse plus longue :

Une table de hachage a des temps de consultation PRESQUE constants et, dans le monde réel, pour atteindre un élément dans une table de hachage, il ne suffit pas de calculer un hachage.

Pour accéder à un élément, votre table de hachage fera quelque chose comme ceci :

  • Obtenez le hachage de la clé
  • Obtenez le numéro de seau pour ce hachage (généralement la fonction map ressemble à ceci bucket = hash % bucketsCount)
  • Parcourir la chaîne d'éléments (en fait, c'est une liste d'éléments qui partagent le même seau. le même seau, la plupart des hashtables utilisent cette méthode pour gérer les collisions ) qui commence à ce et comparez chaque clé avec celle de l'élément de l'élément que vous essayez de ajouter/supprimer/mettre à jour/vérifier si contenu.

Les temps de consultation dépendent de la "qualité" de la 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

8voto

Mark Ransom Points 132545

Si votre ensemble d'objets est vraiment statique et immuable, vous pouvez utiliser un fichier hachis parfait pour obtenir des performances garanties O(1). J'ai déjà vu gperf mentionné à quelques reprises, bien que je n'aie jamais eu l'occasion de l'utiliser moi-même.

1 votes

Si vous pouvez placer une limite supérieure constante sur la taille des tout un algorithme ou une structure de données, vous pouvez prétendre à une limite O(1) pour ses performances. Cela se fait souvent dans la réalité - par exemple, les performances de la recherche à l'intérieur d'un nœud d'un arbre B sont considérées comme constantes, puisque (indépendamment de la recherche linéaire ou binaire) la taille maximale d'un nœud est constante. +1 pour une bonne suggestion, mais pour l'affirmation O(1), je pense que vous trichez un peu.

1 votes

@Steve314, je pense que vous ne comprenez pas l'intérêt d'un hachage parfait. En personnalisant la fonction de hachage, vous avez la garantie de ne pas avoir de collisions. une opération pour atteindre les données une fois que vous avez leur hachage, plus une comparaison pour s'assurer que vous ne cherchiez pas quelque chose qui n'est pas dans la table.

0 votes

Mais ce que je veux dire, c'est que vous personnalisez le hachage pour un usage particulier et constant quantité de données. Vous avez tout à fait raison en ce qui concerne les avantages d'un hachage parfait, mais comme il ne peut pas faire face à la variation de n (ou même à la variation des données à l'intérieur de n, d'ailleurs), c'est toujours de la triche.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X