Ces deux semblent très similaires et ont une structure presque identique. Quelle est la différence? Quelles sont les complexités temporelles pour les différentes opérations de chacune?
Ces deux semblent très similaires et ont une structure presque identique. Quelle est la différence? Quelles sont les complexités temporelles pour les différentes opérations de chacune?
Réponses:
Heap garantit simplement que les éléments des niveaux supérieurs sont supérieurs (pour max-heap) ou plus petits (pour min-heap) que ceux des niveaux inférieurs, alors que BST garantit l'ordre (de "gauche" à "droite"). Si vous voulez des éléments triés, utilisez BST. par Dante n'est pas un geek
Heap est meilleur à findMin / findMax (O (1)), tandis que BST est bon à toutes les découvertes (O (logN)). Insert est O (logN) pour les deux structures. Si vous ne vous souciez que de findMin / findMax (par exemple, lié à la priorité), optez pour heap. Si vous voulez tout régler, allez avec BST.
Les arbres de recherche binaires et les tas binaires sont des structures de données basées sur des arbres.
Les tas exigent que les nœuds aient la priorité sur leurs enfants. Dans un tas max, les enfants de chaque nœud doivent être inférieurs à lui-même. C'est le contraire pour un tas min:
Les arbres de recherche binaires (BST) suivent un ordre spécifique (précommande, en ordre, post-commande) entre les nœuds frères. L'arbre doit être trié, contrairement aux tas:
Sommaire
Type BST (*) Heap
Insert average log(n) 1
Insert worst log(n) log(n) or n (***)
Find any worst log(n) n
Find max worst 1 (**) 1
Create worst n log(n) n
Delete worst log(n) log(n)
Tous les temps moyens sur cette table sont les mêmes que leurs temps les pires à l'exception de Insert.
*
: partout dans cette réponse, BST == BST équilibré, car déséquilibré aspire asymptotiquement**
: en utilisant une modification triviale expliquée dans cette réponse***
: log(n)
pour tas de pointeur, n
pour tas dynamiqueAvantages du tas binaire sur un BST
l’insertion moyenne dans un tas binaire est O(1)
, pour BST, est O(log(n))
. Ceci est la caractéristique tueur de tas.
Il y a aussi d'autres tas qui atteignent O(1)
amorti (plus fort) comme le tas de Fibonacci , et même le pire des cas, comme la file d'attente Brodal , bien qu'ils puissent ne pas être pratiques à cause de performances non asymptotiques: https://stackoverflow.com/questions/30782636 / es-fibonacci-tas-ou-brodal-files d'attente-utilisées-dans-la-pratique-n'importe où
Les tas binaires peuvent être efficacement implémentés au-dessus de tableaux dynamiques ou d'arbres basés sur un pointeur, BST uniquement des arbres basés sur un pointeur. Donc, pour le tas, nous pouvons choisir une implémentation de tableau moins encombrante, si nous pouvons nous permettre des latences de redimensionnement occasionnelles.
la création de tas binaire est le O(n)
pire des cas , O(n log(n))
pour BST.
Avantage de la BST sur le tas binaire
la recherche d'éléments arbitraires est O(log(n))
. C'est la caractéristique qui tue les BST.
Pour tas, c'est O(n)
en général, à l'exception du plus grand élément qui est O(1)
.
"Faux" avantage du tas sur BST
tas est O(1)
de trouver max, BST O(log(n))
.
Ceci est une idée fausse commune, car il est trivial de modifier un fichier BST pour garder le suivi du plus grand élément et de le mettre à jour chaque fois que cet élément peut être modifié: lors de l'insertion d'un plus grand échange, le deuxième plus grand à la suppression. https://stackoverflow.com/questions/7878622/can-we-use-binary-search-tree-to-simulate-heap-operation (mentionné par Yeo ).
En réalité, il s’agit d’une limitation des tas comparée aux BST: la seule recherche efficace est celle du plus gros élément.
L'insertion moyenne de tas binaire est O(1)
Sources:
Argument intuitif:
Dans un segment binaire, augmenter la valeur à un index donné revient également O(1)
à la même raison. Mais si vous souhaitez le faire, il est probable que vous souhaitiez conserver un index supplémentaire à jour sur les opérations de segment de mémoire https://stackoverflow.com/questions/17009056/how-to-implement-ologn-decrease- clé-operation-for-min-heap-based-priority-queu, par exemple pour Dijkstra. Possible sans coût supplémentaire de temps.
Indice de référence d'insertion de bibliothèque standard GCC C ++ sur du matériel réel
J'ai comparé les insertions C ++ std::set
( BST arbre rouge-noir ) et std::priority_queue
( tas de tableau dynamique ) pour voir si j'avais raison concernant les temps d'insertion, et voici ce que j'ai obtenu:
Alors clairement:
Le temps d'insertion de tas est fondamentalement constant.
Nous pouvons clairement voir les points de redimensionnement de tableaux dynamiques. Puisque nous faisons la moyenne tous les 10 000 inserts pour pouvoir voir quoi que ce soit au-dessus du bruit du système , ces pics sont en fait environ 10 000 fois plus grands que ceux indiqués!
Le graphique zoomé exclut essentiellement uniquement les points de redimensionnement de la matrice et montre que presque toutes les insertions tombent en dessous de 25 nanosecondes.
BST est logarithmique. Toutes les insertions sont beaucoup plus lentes que l'insertion de tas moyenne.
Analyse détaillée de BST vs hashmap à l' adresse : https://stackoverflow.com/questions/18414579/what-data-structure-is-inside-stdmap-in-c/51945119#51945119
Indice de référence d'insertion de bibliothèque standard GCC C ++ sur gem5
gem5 est un simulateur de système complet, et fournit donc une horloge infiniment précise avec m5 dumpstats
. J'ai donc essayé de l'utiliser pour estimer le minutage d'insertions individuelles.
Interprétation:
heap est toujours constant, mais nous voyons maintenant plus en détail qu'il y a quelques lignes et que chaque ligne la plus haute est plus clairsemée.
Cela doit correspondre aux latences d'accès mémoire effectuées pour les insertions de plus en plus hautes.
TODO Je ne peux pas vraiment interpréter complètement le BST car il n’a pas l’air si logarithmique et un peu plus constant.
Avec ces détails plus détaillés, nous pouvons toutefois voir quelques lignes distinctes, mais je ne suis pas sûr de ce qu’elles représentent: je m'attendrais à ce que la ligne de fond soit plus fine, puisque nous insérons en haut en bas?
Benchmarked avec cette installation Buildroot sur un processeur HPI aarch64 .
BST ne peut pas être efficacement implémenté sur un tableau
Les opérations de segment de mémoire n'ont besoin que de bouillonner vers le haut ou le bas d'une seule branche d'arborescence. Dans le O(log(n))
pire des cas, les échanges sont donc O(1)
moyens.
Garder un BST équilibré nécessite des rotations d’arbres, ce qui peut remplacer l’élément supérieur par un autre et nécessiter le déplacement de tout le tableau ( O(n)
).
Les tas peuvent être efficacement implémentés sur un tableau
Les index parents et enfants peuvent être calculés à partir de l'index actuel, comme indiqué ici .
Il n'y a pas d'opérations d'équilibrage comme BST.
Supprimer min est l'opération la plus inquiétante car elle doit être descendante. Mais cela peut toujours être fait en "infiltrant" une seule branche du tas, comme expliqué ici . Cela conduit à un cas pire O (log (n)), car le tas est toujours bien équilibré.
Si vous insérez un seul nœud pour chaque nœud supprimé, vous perdez l'avantage de l'insertion moyenne asymptotique O (1) fournie par les tas car la suppression l'emporterait, et vous pourriez également utiliser un fichier BST. Cependant, Dijkstra met à jour les nœuds plusieurs fois pour chaque suppression, donc tout va bien.
Tas de tableaux dynamiques vs tas de pointeurs
Les tas peuvent être efficacement mis en œuvre au-dessus des tas de pointeurs: https://stackoverflow.com/questions/19720438/is-it-possible-to-make-efficient-pointer-based-binary-heap-implementations
L'implémentation de tableau dynamique est plus efficace en termes d'espace. Supposons que chaque élément de tas ne contienne qu'un pointeur sur un struct
:
l'implémentation de l'arborescence doit stocker trois pointeurs pour chaque élément: parent, enfant gauche et enfant droit. Donc, l'utilisation de la mémoire est toujours 4n
(3 pointeurs d'arbre + 1 struct
pointeur).
Les BST d'arbres auraient également besoin d'informations d'équilibrage supplémentaires, telles que le noir-rouge.
l'implémentation de tableau dynamique peut être de taille 2n
juste après un doublement. Donc, en moyenne, ça va être 1.5n
.
D'autre part, le tas d'arborescence a une meilleure insertion dans le pire des cas, car copier le tableau dynamique de sauvegarde pour doubler sa taille prend le O(n)
pire des cas, alors que le tas d'arborescence ne fait que de nouvelles petites allocations pour chaque nœud.
Néanmoins, le doublement de la matrice de support est O(1)
amorti, ce qui revient à une considération de latence maximale. Mentionné ici .
Philosophie
Les BST conservent une propriété globale entre un parent et tous les descendants (gauche plus petite, droite plus grande).
Le nœud supérieur d'un BST est l'élément central, ce qui nécessite une connaissance globale pour sa maintenance (savoir combien d'éléments plus petits et plus grands sont présents).
Cette propriété globale est plus coûteuse à gérer (log n insert), mais donne des recherches plus puissantes (log n search).
Les tas gardent une propriété locale entre le parent et les enfants directs (parent> enfants).
La note supérieure d'un segment de mémoire est le gros élément, qui ne nécessite que des connaissances locales à maintenir (connaître votre parent).
Liste doublement chaînée
Une liste doublement chaînée peut être vue comme un sous-ensemble du tas où le premier élément a la priorité la plus grande, comparons-les ici aussi:
O(1)
pire des cas puisque nous avons des pointeurs sur les éléments, et la mise à jour est vraiment simpleO(1)
moyen, donc pire que la liste chaînée. Compromis pour avoir une position d'insertion plus générale.O(n)
pour les deuxUn cas d'utilisation pour cela est lorsque la clé du tas est l'horodatage actuel: dans ce cas, les nouvelles entrées iront toujours au début de la liste. Ainsi, nous pouvons même oublier l'horodatage exact, et garder la position dans la liste comme priorité.
Ceci peut être utilisé pour implémenter un cache LRU . Comme pour les applications de type tas telles que Dijkstra , vous souhaiterez conserver un hashmap supplémentaire de la clé au nœud correspondant de la liste, afin de rechercher le nœud à mettre à jour rapidement.
Comparaison de différents BST équilibrés
Bien que l'insertion asymptotique et les temps de recherche de toutes les structures de données classées comme "BST équilibrées" que j'ai vus jusqu'à présent soient identiques, différents BBST ont des compromis différents. Je n'ai pas encore étudié cela à fond, mais il serait bon de résumer ces compromis ici:
Voir également
Question similaire sur CS: Quelle est la différence entre un arbre de recherche binaire et un tas binaire?
Avec la structure de données, il faut distinguer les niveaux de préoccupation.
Les structures de données abstraites (objets stockés, leurs opérations) dans cette question sont différentes. L'un implémente une file d'attente prioritaire, l'autre un ensemble. Une file d'attente prioritaire n'est pas intéressée par la recherche d'un élément arbitraire, mais uniquement de celui dont la priorité est la plus grande.
La mise en œuvre concrète des structures. À première vue, ce sont tous deux des arbres (binaires), avec des propriétés structurelles différentes. L'ordre relatif des clés et les structures globales possibles diffèrent. (Un peu imprécis, dans une BST
clé, les commandes sont ordonnées de gauche à droite, dans un segment, elles sont ordonnées de haut en bas.) Comme le remarque correctement IPlant, un segment doit également être "complet".
Il y a une différence finale dans la mise en œuvre de bas niveau . Un arbre de recherche binaire (non équilibré) a une implémentation standard utilisant des pointeurs. Un tas binaire, au contraire, a une implémentation efficace utilisant un tableau (précisément à cause de la structure restreinte).