web-dev-qa-db-fra.com

Arbres binaires vs listes liées vs tables de hachage

Je crée une table des symboles pour un projet sur lequel je travaille. Je me demandais quelles sont les opinions des gens sur les avantages et les inconvénients des différentes méthodes disponibles pour stocker et créer une table des symboles.

J'ai fait pas mal de recherches et les plus couramment recommandées sont les arbres binaires ou les listes liées ou les tables de hachage. Quels sont les avantages et/ou les inconvénients de tout ce qui précède? (travailler en c ++)

72
benofsky

Votre cas d'utilisation va probablement être "insérer les données une fois (par exemple, le démarrage de l'application), puis effectuer de nombreuses lectures mais peu ou pas d'insertions supplémentaires".

Par conséquent, vous devez utiliser un algorithme rapide pour rechercher les informations dont vous avez besoin.

Je pense donc que le HashTable était l'algorithme le plus approprié à utiliser, car il génère simplement un hachage de votre objet clé et l'utilise pour accéder aux données cibles - c'est O (1). Les autres sont O(N) (Listes liées de taille N - vous devez parcourir la liste une par une, une moyenne de N/2 fois) et O (log N) ( Arbre binaire - vous divisez par deux l'espace de recherche à chaque itération - uniquement si l'arborescence est équilibrée, cela dépend donc de votre implémentation, un arbre déséquilibré peut avoir des performances nettement moins bonnes).

Assurez-vous simplement qu'il y a suffisamment d'espaces (compartiments) dans le HashTable pour vos données (R.e., commentaire de Soraz sur ce post). La plupart des implémentations de framework (Java, .NET, etc.) seront d'une qualité que vous n'aurez pas à vous soucier des implémentations.

Avez-vous suivi un cours sur les structures de données et les algorithmes à l'université?

48
JeeBee

Les compromis standard entre ces structures de données s'appliquent.

  • Arbres binaires
    • complexité moyenne à implémenter (en supposant que vous ne pouvez pas les obtenir d'une bibliothèque)
    • les insertions sont O (logN)
    • les recherches sont O (logN)
  • Listes liées (non triées)
    • faible complexité à mettre en œuvre
    • les inserts sont O (1)
    • les recherches sont O (N)
  • Tables de hachage
    • haute complexité à mettre en œuvre
    • les insertions sont O(1) en moyenne
    • les recherches sont O(1) en moyenne
74
Darron

Ce que tout le monde semble oublier, c'est que pour les petits N, IE quelques symboles dans votre table, la liste chaînée peut être beaucoup plus rapide que la table de hachage, bien qu'en théorie sa complexité asymptotique soit en effet plus élevée.

Il y a une fameuse qoute des notes de Pike sur la programmation en C: "Règle 3. Les algorithmes de fantaisie sont lents lorsque n est petit, et n est généralement petit. Les algorithmes de fantaisie ont de grandes constantes. Jusqu'à ce que vous sachiez que n va souvent être grand, ne vous en faites pas. " http://www.lysator.liu.se/c/pikestyle.html

Je ne peux pas dire à partir de votre message si vous aurez affaire à un petit N ou non, mais rappelez-vous toujours que le meilleur algorithme pour les grands N n'est pas nécessairement bon pour les petits N.

42

Il semble que ce qui suit peut être vrai:

  • Vos clés sont des chaînes.
  • Les insertions sont effectuées une seule fois.
  • Les recherches sont effectuées fréquemment.
  • Le nombre de paires clé-valeur est relativement faible (disons, moins d'un K ou plus).

Si c'est le cas, vous pourriez envisager une liste triée sur l'une de ces autres structures. Cela fonctionnerait moins bien que les autres lors des insertions, car une liste triée est O(N) lors de l'insertion, contre O(1) pour une liste chaînée ou table de hachage et O (log2N) pour un arbre binaire équilibré. Mais les recherches dans une liste triée peuvent être plus rapides que n'importe laquelle de ces autres structures (je l'expliquerai brièvement), vous pouvez donc sortir en tête. De plus, si vous effectuez toutes vos insertions à la fois (ou si vous n'avez pas besoin de recherches jusqu'à ce que toutes les insertions soient terminées), vous pouvez simplifier les insertions à O(1) et effectuer un tri beaucoup plus rapide) à la fin. De plus, une liste triée utilise moins de mémoire que n'importe laquelle de ces autres structures, mais la seule façon dont cela peut avoir de l'importance est si vous avez plusieurs petites listes. Si vous avez une ou quelques grandes listes, alors un hachage table est susceptible de surpasser une liste triée.

Pourquoi les recherches pourraient-elles être plus rapides avec une liste triée? Eh bien, il est clair que c'est plus rapide qu'une liste chaînée, avec le temps de recherche de ce dernier O(N). Avec un arbre binaire, les recherches ne restent que O (log2 N) si l'arbre reste parfaitement équilibré. Garder l'arbre équilibré (rouge-noir, par exemple) ajoute à la complexité et au temps d'insertion. De plus, avec les listes liées et les arbres binaires, chaque élément est un attribut séparé1  node , ce qui signifie que vous devrez déréférencer les pointeurs et probablement passer à des adresses mémoire potentiellement très variables, augmentant ainsi les chances de manquer un cache.

En ce qui concerne les tables de hachage, vous devriez probablement lire n couple sur autres questions ici sur StackOverflow, mais les principaux points d'intérêt ici sont:

  • Une table de hachage peut dégénérer en O(N) dans le pire des cas.
  • Le coût du hachage est non nul et, dans certaines implémentations, il peut être important, en particulier dans le cas des chaînes.
  • Comme dans les listes liées et les arbres binaires, chaque entrée est un nœud stockant plus que la clé et la valeur, également allouées séparément dans certaines implémentations, vous utilisez donc plus de mémoire et augmentez les chances de manquer un cache.

Bien sûr, si vous vous souciez vraiment de la performance de ces structures de données, vous devez les tester. Vous ne devriez pas avoir de problème à trouver de bonnes implémentations pour ces langages dans la plupart des langues courantes. Il ne devrait pas être trop difficile de jeter certaines de vos données réelles sur chacune de ces structures de données et de voir celle qui fonctionne le mieux.

  1. Il est possible pour une implémentation de pré-allouer un tableau de nœuds, ce qui aiderait à résoudre le problème de manque de cache. Je n'ai vu cela dans aucune implémentation réelle de listes liées ou d'arbres binaires (pas que j'en ai vu chacun, bien sûr), bien que vous puissiez certainement rouler le vôtre. Cependant, vous auriez toujours une possibilité légèrement plus élevée de manquer un cache, car les objets du nœud seraient nécessairement plus grands que les paires clé/valeur.
8
P Daddy

J'aime la réponse de Bill, mais elle ne synthétise pas vraiment les choses.

Parmi les trois choix:

Les listes liées sont relativement lentes à rechercher des éléments dans (O (n)). Donc, si vous avez beaucoup d'éléments dans votre table, ou si vous allez faire beaucoup de recherches, ils ne sont pas le meilleur choix. Cependant, ils sont faciles à construire et à écrire également. Si la table est petite et/ou si vous ne la parcourez qu’une fois après sa construction, cela peut être le choix pour vous.

Les tables de hachage peuvent être extrêmement rapides. Cependant, pour que cela fonctionne, vous devez choisir un bon hachage pour votre entrée, et vous devez choisir une table assez grande pour contenir tout sans beaucoup de collisions de hachage. Cela signifie que vous devez savoir quelque chose sur la taille et la quantité de votre entrée. Si vous vous trompez, vous vous retrouvez avec un ensemble très coûteux et complexe de listes liées. Je dirais qu'à moins que vous ne sachiez à l'avance à peu près la taille de la table, n'utilisez pas de table de hachage. Cela contredit votre réponse "acceptée". Désolé.

Cela laisse des arbres. Vous avez cependant une option ici: équilibrer ou ne pas équilibrer. Ce que j'ai trouvé en étudiant ce problème sur le code C et Fortran que nous avons ici, c'est que l'entrée de la table des symboles a tendance à être suffisamment aléatoire pour que vous ne perdiez qu'un ou deux niveaux d'arbre en n'équilibrant pas l'arbre. Étant donné que les arbres équilibrés sont plus lents à insérer des éléments et sont plus difficiles à mettre en œuvre, je ne m'embêterais pas avec eux. Cependant, si vous avez déjà accès aux bibliothèques de composants débogués de Nice (par exemple: STL de C++), vous pouvez aussi bien continuer et utiliser l'arborescence équilibrée.

7
T.E.D.

Quelques choses à surveiller.

  • Les arbres binaires n'ont une recherche O (log n) et une complexité d'insertion que si l'arbre est équilibré . Si vos symboles sont insérés de manière assez aléatoire, cela ne devrait pas poser de problème. S'ils sont insérés dans l'ordre, vous allez créer une liste chaînée. (Pour votre application spécifique, ils ne devraient pas être dans n'importe quel ordre, donc vous devriez être d'accord.) S'il y a une chance que les symboles soient trop ordonnés, un Rouge-Noir L'arbre est un meilleur option.

  • Les tables de hachage donnent O(1) complexité moyenne d'insertion et de recherche, mais il y a aussi une mise en garde ici. Si votre fonction de hachage est mauvaise (et je veux dire vraiment mauvais) vous pourriez également finir par construire une liste chaînée. Toute fonction de hachage de chaîne raisonnable devrait le faire, donc cet avertissement ne sert vraiment qu'à vous assurer que vous êtes conscient que cela pourrait se produire . Vous devriez pouvoir tester simplement que votre fonction de hachage n'a pas beaucoup de collisions sur la plage attendue d'entrées, et tout ira bien. Un autre inconvénient mineur est que si vous utilisez une table de hachage de taille fixe. La plupart Les implémentations de table de hachage augmentent lorsqu'elles atteignent une certaine taille (facteur de charge pour être plus précis, voir ici pour plus de détails). Ceci afin d'éviter le problème que vous obtenez lorsque vous insérez un million de symboles dans dix compartiments Cela conduit à dix listes chaînées d'une taille moyenne de 100 000.

  • Je n'utiliserais une liste chaînée que si j'avais une table de symboles vraiment courte. Il est plus facile à mettre en œuvre, mais les meilleures performances de cas pour une liste chaînée sont les pires performances de cas pour vos deux autres options.

6
Bill the Lizard

D'autres commentaires se sont concentrés sur l'ajout/la récupération d'éléments, mais cette discussion n'est pas complète sans considérer ce qu'il faut pour parcourir toute la collection. La réponse courte ici est que les tables de hachage nécessitent moins de mémoire pour parcourir, mais les arbres nécessitent moins de temps.

Pour une table de hachage, la surcharge de mémoire d'itération sur les paires (clé, valeur) ne dépend pas de la capacité de la table ou du nombre d'éléments stockés dans la table; en fait, l'itération ne devrait nécessiter qu'une ou deux variables d'index.

Pour les arbres, la quantité de mémoire requise dépend toujours de la taille de l'arbre. Vous pouvez soit maintenir une file d'attente de nœuds non visités pendant l'itération, soit ajouter des pointeurs supplémentaires à l'arborescence pour une itération plus facile (faire en sorte que l'arborescence, à des fins d'itération, agisse comme une liste liée), mais dans tous les cas, vous devez allouer de la mémoire supplémentaire pour l'itération .

Mais la situation est inversée en ce qui concerne le calendrier. Pour une table de hachage, le temps nécessaire pour itérer dépend de la capacité de la table et non du nombre d'éléments stockés. Ainsi, une table chargée à 10% de sa capacité prendra environ 10 fois plus de temps à parcourir qu'une liste chaînée avec les mêmes éléments!

1
anonymous

Cela dépend bien sûr de plusieurs choses. Je dirais qu'une liste chaînée est tout de suite sortie, car elle a peu de propriétés appropriées pour fonctionner comme une table de symboles. Un arbre binaire peut fonctionner, si vous en avez déjà un et que vous n'avez pas à passer du temps à l'écrire et à le déboguer. Mon choix serait une table de hachage, je pense que c'est plus ou moins la valeur par défaut à cet effet.

0
unwind

Cette question passe par les différents conteneurs en C #, mais ils sont similaires dans toutes les langues que vous utilisez.

0
Mats Fredriksson

À moins que vous ne vous attendiez à ce que votre table de symboles soit petite, je devrais éviter les listes liées. Une liste de 1000 articles prendra en moyenne 500 itérations pour trouver n'importe quel élément en son sein.

Un arbre binaire peut être beaucoup plus rapide, tant qu'il est équilibré. Si vous persistez le contenu, le formulaire sérialisé sera probablement trié, et quand il sera rechargé, l'arborescence résultante sera complètement déséquilibrée en conséquence, et elle se comportera de la même manière que la liste liée - parce que c'est essentiellement ce qu'il est devenu. Les algorithmes d'arbres équilibrés résolvent ce problème, mais rendent l'ensemble du Shebang plus complexe.

Une table de hachage (tant que vous choisissez un algorithme de hachage approprié) ressemble à la meilleure solution. Vous n'avez pas mentionné votre environnement, mais à peu près tous les langages modernes ont un Hashmap intégré.

0
Martin Cowie