Pourquoi le C ++ STL ne fournit-il pas de conteneurs «arborescents»?

373

Pourquoi le C ++ STL ne fournit-il pas de conteneurs "arborescents", et quelle est la meilleure chose à utiliser à la place?

Je veux stocker une hiérarchie d'objets sous forme d'arbre, plutôt que d'utiliser un arbre comme amélioration des performances ...

Roddy
la source
7
J'ai besoin d'un arbre pour stocker une représentation d'une hiérarchie.
Roddy
20
Je suis avec le gars qui a voté contre les «bonnes» réponses, ce qui semble être le cas; "Les arbres sont inutiles". Il existe des utilisations importantes, quoique obscures, des arbres.
Joe Soul-bringer
Je pense que la raison est triviale - personne ne l'a encore implémenté dans la bibliothèque standard. C'est comme si la bibliothèque standard n'en avait pas std::unordered_mapet std::unordered_setjusqu'à récemment. Et avant cela, il n'y avait aucun conteneur STL dans la bibliothèque standard.
doc
1
Mes pensées (n'ayant jamais lu la norme pertinente, donc c'est un commentaire et non une réponse) sont que la STL ne se soucie pas des structures de données spécifiques, elle se soucie des spécifications concernant la complexité et quelles opérations sont prises en charge. La structure sous-jacente utilisée peut donc varier entre les implémentations et / ou les architectures cibles, à condition qu'elle satisfasse aux spécifications. Je suis à peu près sûr std::mapet std::setutiliserai un arbre dans chaque implémentation, mais ils n'ont pas à le faire si une structure non arborescente répond également aux spécifications.
Mark K Cowan

Réponses:

182

Il y a deux raisons pour lesquelles vous pourriez vouloir utiliser un arbre:

Vous voulez refléter le problème en utilisant une structure arborescente:
pour cela, nous avons une bibliothèque de graphiques boost

Ou vous voulez un conteneur qui a des caractéristiques d'accès en forme d'arbre Pour cela, nous avons

Fondamentalement, les caractéristiques de ces deux conteneurs sont telles qu'ils doivent pratiquement être mis en œuvre à l'aide d'arbres (bien que ce ne soit pas réellement une exigence).

Voir aussi cette question: Implémentation de l'arborescence C

Martin York
la source
64
Il existe de nombreuses raisons d'utiliser un arbre, même si ce sont les plus courantes. Le plus commun! Égal à tous.
Joe Soul-bringer
3
Une troisième raison majeure de vouloir un arbre est pour une liste toujours triée avec une insertion / suppression rapide, mais pour cela il y a std: multiset.
VoidStar
1
@Durga: vous ne savez pas en quoi la profondeur est pertinente lorsque vous utilisez la carte comme conteneur trié. La carte garantit l'insertion / suppression / recherche de log (n) (et contenant les éléments dans l'ordre trié). C'est toute la carte qui est utilisée et implémentée (généralement) comme un arbre rouge / noir. Un arbre rouge / noir s'assure que l'arbre est équilibré. La profondeur de l'arbre est donc directement liée au nombre d'éléments dans l'arbre.
Martin York
14
Je ne suis pas d'accord avec cette réponse, à la fois en 2008 et maintenant. La bibliothèque standard n'a pas "boost", et la disponibilité de quelque chose dans boost ne devrait pas (et n'a pas été) une raison pour ne pas l'adopter dans la norme. De plus, le BGL est général et suffisamment impliqué pour mériter des classes d'arbre spécialisées indépendantes de celui-ci. De plus, le fait que std :: map et std :: set nécessitent un arbre est, IMO, un autre argument pour avoir un stl::red_black_treeetc. Enfin, les arbres std::mapet std::setsont équilibrés, un std::treepeut ne pas l'être.
einpoklum
1
@einpoklum: "la disponibilité de quelque chose dans boost ne devrait pas être une raison pour ne pas l'adopter dans la norme" - étant donné que l'un des objectifs de boost est de servir de terrain d'essai pour des bibliothèques utiles avant l'incorporation dans la norme, je ne peux que dites "absolument!".
Martin Bonner soutient Monica le
94

Probablement pour la même raison qu'il n'y a pas de conteneur d'arbre en boost. Il existe de nombreuses façons de mettre en œuvre un tel conteneur, et il n'y a pas de bon moyen de satisfaire tous ceux qui l'utiliseraient.

Quelques points à considérer:

  • Le nombre d'enfants pour un nœud est-il fixe ou variable?
  • Combien de frais généraux par nœud? - c'est-à-dire, avez-vous besoin de pointeurs parents, de pointeurs frères, etc.
  • Quels algorithmes proposer? - différents itérateurs, algorithmes de recherche, etc.

En fin de compte, le problème finit par être qu'un conteneur d'arbre qui serait suffisamment utile à tout le monde serait trop lourd pour satisfaire la plupart des personnes qui l'utilisent. Si vous recherchez quelque chose de puissant, Boost Graph Library est essentiellement un surensemble de ce à quoi une bibliothèque d'arborescence pourrait être utilisée.

Voici quelques autres implémentations d'arbres génériques:

Greg Rogers
la source
5
"... pas un bon moyen de satisfaire tout le monde ..." Sauf que puisque stl :: map, stl :: multimap et stl :: set sont basés sur rb_tree de stl, il devrait satisfaire autant de cas que ces types de base. .
Catskul
44
Étant donné qu'il n'y a aucun moyen de récupérer les enfants d'un nœud d'un std::map, je n'appellerais pas ces conteneurs d'arborescence. Ce sont des conteneurs associatifs qui sont généralement mis en œuvre sous forme d'arbres. Grande différence.
Mooing Duck
2
Je suis d'accord avec Mooing Duck, comment implémenteriez-vous une première recherche étendue sur une carte std ::? Ça va être terriblement cher
Marco A.
1
J'ai commencé à utiliser tree.hh de Kasper Peeters, mais après avoir examiné la licence pour GPLv3, ou toute autre version GPL, cela contaminerait notre logiciel commercial. Je recommanderais de regarder treetree fourni dans le commentaire de @hplbsh si vous avez besoin d'une structure à des fins commerciales.
Jake88
3
Les exigences spécifiques aux variétés sur les arbres sont un argument pour avoir différents types d'arbres, pas du tout.
André
50

La philosophie de la STL est que vous choisissez un conteneur en fonction de garanties et non en fonction de la façon dont le conteneur est mis en œuvre. Par exemple, votre choix de conteneur peut être basé sur un besoin de recherches rapides. Pour tous vos soucis, le conteneur peut être implémenté comme une liste unidirectionnelle - tant que la recherche est très rapide, vous seriez heureux. C'est parce que vous ne touchez pas aux internes de toute façon, vous utilisez des itérateurs ou des fonctions membres pour l'accès. Votre code n'est pas lié à la façon dont le conteneur est implémenté, mais à sa vitesse, à son ordre fixe et défini, à son efficacité sur l'espace, etc.

wilhelmtell
la source
12
Je ne pense pas qu'il parle d'implémentations de conteneurs, il parle d'un conteneur d'arbre réel lui-même.
Mooing Duck
3
@MooingDuck Je pense que ce que veut dire wilhelmtell est que la bibliothèque standard C ++ ne définit pas de conteneurs en fonction de leur structure de données sous-jacente; il ne définit les conteneurs que par leur interface et leurs caractéristiques observables comme les performances asymptotiques. Quand on y pense, un arbre n'est pas vraiment un conteneur (comme nous les connaissons) du tout. Ils n'ont même pas un élément simple end()et begin()avec lequel vous pouvez parcourir tous les éléments, etc.
Jordan Melo
7
@JordanMelo: un non-sens sur tous les points. C'est une chose qui contient des objets. Il est très trivial de le concevoir pour avoir des itérateurs begin () et end () et bidirectionnels avec lesquels itérer. Chaque conteneur a des caractéristiques différentes. Il serait utile que l'on puisse en plus avoir des caractéristiques d'arbre. Ça devrait être assez facile.
Mooing Duck
Ainsi, on veut avoir un conteneur qui fournit des recherches rapides pour les nœuds enfants et parents, et des exigences de mémoire raisonnables.
doc
@JordanMelo: De ce point de vue, les adaptateurs comme les files d'attente, les piles ou les files d'attente prioritaires n'appartiendraient pas non plus à la STL (ils n'ont pas non plus begin()et end()). Et rappelez-vous qu'une file d'attente prioritaire est généralement un tas, qui au moins en théorie est un arbre (même si les implémentations réelles). Ainsi, même si vous avez implémenté une arborescence en tant qu'adaptateur en utilisant une structure de données sous-jacente différente, elle pourrait être incluse dans la STL.
andreee
48

"Je veux stocker une hiérarchie d'objets sous forme d'arbre"

C ++ 11 est venu et est parti et ils ne voyaient toujours pas la nécessité de fournir un std::tree, bien que l'idée soit venue (voir ici ). Peut-être que la raison pour laquelle ils n'ont pas ajouté cela est qu'il est trivialement facile de construire le vôtre au-dessus des conteneurs existants. Par exemple...

template< typename T >
struct tree_node
   {
   T t;
   std::vector<tree_node> children;
   };

Une traversée simple utiliserait la récursivité ...

template< typename T >
void tree_node<T>::walk_depth_first() const
   {
   cout<<t;
   for ( auto & n: children ) n.walk_depth_first();
   }

Si vous voulez maintenir une hiérarchie et que vous voulez qu'elle fonctionne avec les algorithmes STL , alors les choses peuvent se compliquer. Vous pouvez créer vos propres itérateurs et obtenir une certaine compatibilité, mais de nombreux algorithmes n'ont tout simplement aucun sens pour une hiérarchie (tout ce qui change l'ordre d'une plage, par exemple). Même la définition d' une plage au sein d'une hiérarchie pourrait être une entreprise compliquée.

nobar
la source
2
Si le projet peut permettre de trier les enfants d'un nœud_arbre, l'utilisation d'un std :: set <> à la place du std :: vector <>, puis l'ajout d'un opérateur <() à l'objet tree_node s'améliorera considérablement. performances de «recherche» d'un objet de type «T».
J Jorgenson
4
Il s'avère qu'ils étaient paresseux et ont fait de votre premier exemple un comportement indéfini.
user541686
2
@ Mehrdad: J'ai finalement décidé de demander le détail de votre commentaire ici .
nobar
many of the algorithms simply don't make any sense for a hierarchy. Une question d'interprétation. Imaginez une structure d'utilisateurs stackoverflow et chaque année, vous voulez que ceux qui ont le plus de points de réputation dirigent ceux qui ont des points de réputation plus bas. Fournissant ainsi un itérateur BFS et une comparaison appropriée, chaque année que vous exécutez std::sort(tree.begin(), tree.end()).
doc
De la même manière, vous pouvez facilement créer une arborescence associative (pour modéliser des enregistrements de valeur-clé non structurés, comme JSON par exemple) en les remplaçant vectorpar mapdans l'exemple ci-dessus. Pour une prise en charge complète d'une structure de type JSON, vous pouvez utiliser variantpour définir les nœuds.
nobar
43

Si vous recherchez une implémentation de l'arborescence RB, stl_tree.h peut également vous convenir.

défaillance du système
la source
14
Étrangement, c'est la seule réponse qui répond réellement à la question d'origine.
Catskul
12
Considérant qu'il veut une "Heiarchie", il semble prudent de supposer que tout ce qui est "équilibré" n'est pas la bonne réponse.
Mooing Duck
11
"Il s'agit d'un fichier d'en-tête interne, inclus par d'autres en-têtes de bibliothèque. Vous ne devez pas essayer de l'utiliser directement."
Dan
3
@Dan: La copier ne signifie pas l'utiliser directement.
einpoklum
12

la carte std :: est basée sur un arbre rouge noir . Vous pouvez également utiliser d'autres conteneurs pour vous aider à implémenter vos propres types d'arbres.

JJ
la source
13
Il utilise généralement des arbres rouge-noir (ce n'est pas obligatoire pour le faire).
Martin York
1
GCC utilise un arbre pour implémenter la carte. Quelqu'un veut-il consulter son répertoire VC include pour voir ce que Microsoft utilise?
JJ
// Classe d'arbre rouge-noir, conçue pour être utilisée dans l'implémentation de conteneurs associatifs STL // (set, multiset, map et multimap). Je l'ai récupéré dans mon fichier stl_tree.h.
JJ
@JJ Au moins dans Studio 2010, il utilise une ordered red-black tree of {key, mapped} values, unique keysclasse interne , définie dans <xtree>. N'ayez pas accès à une version plus moderne pour le moment.
Justin Time - Rétablir Monica le
8

D'une certaine manière, std :: map est un arbre (il doit avoir les mêmes caractéristiques de performance qu'un arbre binaire équilibré) mais il n'expose pas d'autres fonctionnalités d'arbre. Le raisonnement probable derrière l'inclusion d'une véritable structure de données d'arbre était probablement juste une question de ne pas tout inclure dans la stl. La stl peut être considérée comme un cadre à utiliser pour implémenter vos propres algorithmes et structures de données.

En général, s'il y a une fonctionnalité de bibliothèque de base que vous souhaitez, qui n'est pas dans la stl, le correctif est de regarder BOOST .

Sinon, il y a un tas de bibliothèques sur il , en fonction des besoins de votre arbre.

Éclipse
la source
6

Tous les conteneurs STL sont représentés en externe comme des "séquences" avec un mécanisme d'itération. Les arbres ne suivent pas cet idiome.

Emilio Garavaglia
la source
7
Une structure de données arborescente pourrait fournir une traversée en précommande, en ordre ou post-commande via des itérateurs. En fait, c'est ce que fait std :: map.
Andrew Tomazos
3
Oui et non ... cela dépend de ce que vous entendez par "arbre". std::mapest implémenté en interne comme btree, mais en externe, il apparaît comme une SEQUENCE triée de PAIRES. Étant donné tout élément, vous pouvez universellement demander qui est avant et qui est après. Une arborescence générale contenant des éléments dont chacun contient d'autres n'impose aucun tri ou direction. Vous pouvez définir des itérateurs qui parcourent une structure arborescente de plusieurs façons (sallow | deep first | last ...) mais une fois que vous l'avez fait, un std::treeconteneur doit renvoyer l'un d'eux à partir d'une beginfonction. Et il n'y a aucune raison évidente de retourner l'un ou l'autre.
Emilio Garavaglia
4
Un std :: map est généralement représenté par un arbre de recherche binaire équilibré, pas un arbre B. Le même argument que vous avez fait pourrait s'appliquer à std :: unordered_set, il n'a pas d'ordre naturel, mais présente des itérateurs de début et de fin. L'exigence de début et de fin est simplement qu'elle itère tous les éléments dans un ordre déterministe, et non qu'il doit y en avoir un naturel. précommande est un ordre d'itération parfaitement valide pour un arbre.
Andrew Tomazos
4
L'implication de votre réponse est qu'il n'y a pas de structure de données stl n-tree car elle n'a pas d'interface "séquence". C'est tout simplement incorrect.
Andrew Tomazos
3
@EmiloGaravaglia: Comme en témoigne std::unordered_set, qui n'a pas de "façon unique" d'itérer ses membres (en fait, l'ordre d'itération est pseudo-aléatoire et l'implémentation définie), mais est toujours un conteneur stl - cela réfute votre argument. Itérer sur chaque élément d'un conteneur est toujours une opération utile, même si l'ordre n'est pas défini.
Andrew Tomazos
4

Parce que la STL n'est pas une bibliothèque "tout". Il contient, essentiellement, les structures minimales nécessaires pour construire des choses.

Paul Nathan
la source
13
Les arbres binaires sont une fonctionnalité extrêmement basique, et en fait, plus basique que d'autres conteneurs puisque des types comme std :: map, std :: multimap et stl :: set. Étant donné que ces types sont basés sur eux, vous vous attendez à ce que le type sous-jacent soit exposé.
Catskul
2
Je ne pense pas que l'OP demande un arbre binaire , il demande un arbre pour stocker une hiérarchie.
Mooing Duck
Non seulement cela, ajouter un "conteneur" d'arbre à STL aurait signifié ajouter beaucoup de nouveaux concepts, par exemple un navigateur d'arbre (généralisant Iterator).
alfC
5
"Structures minimales pour construire des choses" est une déclaration très subjective. Vous pouvez construire des choses avec des concepts C ++ bruts, donc je pense que le vrai minimum ne serait pas du tout STL.
doc
3

OMI, une omission. Mais je pense qu'il y a une bonne raison de ne pas inclure une structure arborescente dans la STL. Il y a beaucoup de logique dans la maintenance d'une arborescence, qui est mieux écrite en tant que fonctions membres dans l' TreeNodeobjet de base . Quand TreeNodeest enveloppé dans un en-tête STL, il devient plus compliqué.

Par exemple:

template <typename T>
struct TreeNode
{
  T* DATA ; // data of type T to be stored at this TreeNode

  vector< TreeNode<T>* > children ;

  // insertion logic for if an insert is asked of me.
  // may append to children, or may pass off to one of the child nodes
  void insert( T* newData ) ;

} ;

template <typename T>
struct Tree
{
  TreeNode<T>* root;

  // TREE LEVEL functions
  void clear() { delete root ; root=0; }

  void insert( T* data ) { if(root)root->insert(data); } 
} ;
bobobobo
la source
7
C'est beaucoup de posséder des pointeurs bruts que vous avez là, dont beaucoup n'ont pas besoin d'être des pointeurs du tout.
Mooing Duck
Je vous suggère de retirer cette réponse. Une classe TreeNode fait partie d'une implémentation d'arborescence.
einpoklum
3

Je pense qu'il y a plusieurs raisons pour lesquelles il n'y a pas d'arbres STL. Les arbres sont principalement une forme de structure de données récursive qui, comme un conteneur (liste, vecteur, ensemble), a une structure fine très différente qui rend les choix corrects délicats. Ils sont également très faciles à construire sous forme de base à l'aide de la STL.

Un arbre enraciné fini peut être considéré comme un conteneur qui a une valeur ou une charge utile, par exemple une instance d'une classe A et, une collection éventuellement vide d'arbres (sous) enracinés; les arbres avec une collection vide de sous-arbres sont considérés comme des feuilles.

template<class A>
struct unordered_tree : std::set<unordered_tree>, A
{};

template<class A>
struct b_tree : std::vector<b_tree>, A
{};

template<class A>
struct planar_tree : std::list<planar_tree>, A
{};

Il faut penser un peu à la conception de l'itérateur, etc. et aux opérations de produit et de coproduit qui permettent de définir et d'être efficace entre les arbres - et la STL d'origine doit être bien écrite - de sorte que l'ensemble vide, le vecteur ou le conteneur de liste soit vraiment vide de toute charge utile dans le cas par défaut.

Les arbres jouent un rôle essentiel dans de nombreuses structures mathématiques (voir les articles classiques de Butcher, Grossman et Larsen; aussi les articles de Connes et Kriemer pour des exemples de leur assemblage, et comment ils sont utilisés pour énumérer). Il n'est pas correct de penser que leur rôle est simplement de faciliter certaines autres opérations. Ils facilitent plutôt ces tâches en raison de leur rôle fondamental en tant que structure de données.

Cependant, en plus des arbres, il existe également des «co-arbres»; les arbres ont surtout la propriété que si vous supprimez la racine, vous supprimez tout.

Considérez les itérateurs sur l'arborescence, ils seraient probablement réalisés comme une simple pile d'itérateurs, vers un nœud et son parent, ... jusqu'à la racine.

template<class TREE>
struct node_iterator : std::stack<TREE::iterator>{
operator*() {return *back();}
...};

Cependant, vous pouvez en avoir autant que vous le souhaitez; collectivement, ils forment un "arbre" mais là où toutes les flèches circulent dans la direction de la racine, ce co-arbre peut être itéré à travers des itérateurs vers l'itérateur et la racine triviaux; cependant, il ne peut pas être parcouru à travers ou vers le bas (les autres itérateurs ne le connaissent pas), ni l'ensemble des itérateurs ne peut être supprimé sauf en gardant une trace de toutes les instances.

Les arbres sont incroyablement utiles, ils ont beaucoup de structure, ce qui en fait un défi sérieux pour obtenir l'approche définitivement correcte. À mon avis, c'est pourquoi ils ne sont pas mis en œuvre dans la STL. De plus, dans le passé, j'ai vu des gens devenir religieux et trouver l'idée d'un type de conteneur contenant des instances de son propre type difficile - mais ils doivent y faire face - c'est ce que représente un type d'arbre - c'est un nœud contenant un collection éventuellement vide d'arbres (plus petits). Le langage actuel le permet sans problème en fournissant le constructeur par défaut pour container<B>n'alloue pas d'espace sur le tas (ou n'importe où ailleurs) pour un B, etc.

Pour ma part, je serais ravi que cela, dans une bonne forme, trouve sa place dans la norme.

tjl
la source
0

En lisant les réponses ici, les raisons citées sont que l'on ne peut pas parcourir l'arbre ou que l'arbre n'assume pas l'interface similaire aux autres conteneurs STL et qu'on ne peut pas utiliser d'algorithmes STL avec une telle structure d'arbre.

Ayant cela à l'esprit, j'ai essayé de concevoir ma propre structure de données arborescente qui fournira une interface de type STL et sera utilisable avec les algorithmes STL existants autant que possible.

Mon idée était que l'arborescence doit être basée sur les conteneurs STL existants et qu'elle ne doit pas masquer le conteneur, afin qu'il soit accessible à utiliser avec les algorithmes STL.

L'autre caractéristique importante que l'arborescence doit fournir est les itérateurs de déplacement.

Voici ce que j'ai pu trouver: https://github.com/igagis/utki/blob/master/src/utki/tree.hpp

Et voici les tests: https://github.com/igagis/utki/blob/master/tests/tree/tests.cpp

igagis
la source
-9

Tous les conteneurs STL peuvent être utilisés avec des itérateurs. Vous ne pouvez pas avoir un itérateur ou un arbre, parce que vous n'avez pas le droit de passer par l'arbre.

7890
la source
3
Mais vous pouvez dire que BFS ou DFS est la bonne façon. Ou soutenez-les tous les deux. Ou tout autre que vous pouvez imaginer. Dites à l'utilisateur ce que c'est.
tomas789
2
dans std :: map il y a un itérateur d'arbre.
Jai
1
Un arbre pourrait définir son propre type d'itérateur personnalisé qui traverse tous les nœuds dans l'ordre d'un "extrême" à l'autre (c'est-à-dire que pour tout arbre binaire avec les chemins 0 et 1, il pourrait offrir un itérateur qui va de "tous les 0" à "tous" 1s «et un itérateur inverse qui effectue l'inverse, pour un arbre avec une profondeur de 3 et le noeud de départ s, par exemple, on pourrait effectuer une itération sur les noeuds comme s000, s00, s001, s0, s010, s01, s011, s, s100, s10, s101, s1, s110, s11, s111( » plus à gauche » à « droite »), il pourrait également utiliser un modèle de parcours en profondeur ( s, s0, s1, s00, s01, s10, s11,
Justin Time - Rétablir Monica
, etc.), ou tout autre motif, tant qu'il itère sur chaque nœud de telle manière que chacun n'est passé qu'une seule fois.
Justin Time - Rétablir Monica le
1
@doc, très bon point. Je pense std::unordered_setqu'on a "fait" une séquence parce que nous ne connaissons pas de meilleure façon d'itérer sur les éléments que d'une manière arbitraire (donnée en interne par la fonction de hachage). Je pense que c'est le cas inverse de l'arbre: l'itération terminée unordered_setest sous-spécifiée, en théorie il n'y a "aucun moyen" de définir une itération autre que peut-être "au hasard". Dans le cas de l'arbre, il existe de nombreuses "bonnes" façons (non aléatoires). Mais, encore une fois, votre point est valable.
alfC