¿Por qué el STL de C ++ no proporciona ningún contenedor de "árbol"?


373

¿Por qué el STL de C ++ no proporciona ningún contenedor de "árbol", y cuál es la mejor opción?

Quiero almacenar una jerarquía de objetos como un árbol, en lugar de usar un árbol como una mejora del rendimiento ...


77
Necesito un árbol para almacenar una representación de una jerarquía.
Roddy

20
Estoy con el tipo que rechazó las respuestas "correctas", lo que parece ser; "Los árboles son inútiles". Hay usos importantes aunque oscuros de los árboles.
Joe Soul-bringer

Creo que el motivo es trivial: todavía nadie lo implementó en la biblioteca estándar. Es como si la biblioteca estándar no tuviera std::unordered_mapy std::unordered_sethasta hace poco. Y antes de eso no había contenedores STL en la biblioteca estándar.
doc

1
Sin embargo, mi opinión (sin haber leído nunca el estándar relevante, por lo tanto, este es un comentario, no una respuesta) es que al STL no le importan las estructuras de datos específicas, se preocupa por las especificaciones con respecto a la complejidad y qué operaciones son compatibles. Por lo tanto, la estructura subyacente utilizada puede variar entre implementaciones y / o arquitecturas de destino, siempre que cumpla con las especificaciones. Estoy bastante seguro std::mapy std::setusaré un árbol en cada implementación, pero no tienen que hacerlo si alguna estructura que no sea un árbol también cumple con las especificaciones.
Mark K Cowan

Respuestas:


182

Hay dos razones por las que podrías querer usar un árbol:

Desea reflejar el problema utilizando una estructura similar a un árbol:
para esto tenemos una biblioteca de gráficos de impulso

O desea un contenedor que tenga características de acceso tipo árbol. Para esto tenemos

Básicamente, las características de estos dos contenedores son tales que prácticamente tienen que implementarse utilizando árboles (aunque esto no es realmente un requisito).

Vea también esta pregunta: Implementación del árbol C


64
Hay muchas, muchas razones para usar un árbol, incluso si estas son las más comunes. Lo más común: igual a todos.
Joe Soul-bringer

3
Una tercera razón importante para querer un árbol es para una lista siempre ordenada con inserción / eliminación rápida, pero para eso hay std: multiset.
VoidStar

1
@ Durga: No estoy seguro de cómo la profundidad es relevante cuando usa el mapa como un contenedor ordenado. El mapa garantiza la inserción / eliminación de registros (n) / búsqueda (y elementos que contienen en orden). Este es todo el mapa para el que se usa y se implementa (generalmente) como un árbol rojo / negro. Un árbol rojo / negro se asegura de que el árbol esté equilibrado. Entonces, la profundidad del árbol está directamente relacionada con el número de elementos en el árbol.
Martin York

14
No estoy de acuerdo con esta respuesta, tanto en 2008 como ahora. La biblioteca estándar no "tiene" impulso, y la disponibilidad de algo en impulso no debería ser (y no ha sido) una razón para no adoptarlo en el estándar. Además, el BGL es lo suficientemente general e implicado como para merecer clases de árbol especializadas independientes de él. Además, el hecho de que std :: map y std :: set requieren un árbol es, en mi opinión, otro argumento para tener un stl::red_black_treeetc. Finalmente, los árboles std::mapy std::setestán equilibrados, un std::treepodría no serlo.
einpoklum

1
@einpoklum: "la disponibilidad de algo en boost no debería ser una razón para no adoptarlo en el estándar" - dado que uno de los propósitos de boost es actuar como un campo de pruebas para bibliotecas útiles antes de la incorporación al estándar, solo puedo di "absolutamente".
Martin Bonner apoya a Mónica el

94

Probablemente por la misma razón que no hay un contenedor de árbol en boost. Hay muchas formas de implementar dicho contenedor, y no hay una buena manera de satisfacer a todos los que lo usarían.

Algunas cuestiones a tener en cuenta:

  • ¿El número de hijos para un nodo es fijo o variable?
  • ¿Cuánta sobrecarga por nodo? - es decir, ¿necesita punteros para padres, punteros para hermanos, etc.
  • ¿Qué algoritmos proporcionar? - diferentes iteradores, algoritmos de búsqueda, etc.

Al final, el problema termina siendo que un contenedor de árbol que sería lo suficientemente útil para todos, sería demasiado pesado para satisfacer a la mayoría de las personas que lo usan. Si está buscando algo poderoso, Boost Graph Library es esencialmente un superconjunto de para qué podría usarse una biblioteca de árbol.

Aquí hay algunas otras implementaciones de árbol genéricas:


55
"... no hay una buena manera de satisfacer a todos ..." Excepto que, dado que stl :: map, stl :: multimap y stl :: set se basan en stb's rb_tree, debería satisfacer tantos casos como esos tipos básicos .
Catskul

44
Teniendo en cuenta que no hay forma de recuperar los hijos de un nodo de a std::map, no llamaría a esos contenedores de árbol. Esos son contenedores asociativos que comúnmente se implementan como árboles. Gran diferencia.
Mooing Duck

2
Estoy de acuerdo con Mooing Duck, ¿cómo implementaría una primera búsqueda amplia en un mapa std ::? Va a ser terriblemente caro
Marco A.

1
Empecé a usar el árbol de Kasper Peeters.hh, sin embargo, después de revisar la licencia para GPLv3, o cualquier otra versión de GPL, contaminaría nuestro software comercial. Recomendaría mirar treeree proporcionado en el comentario de @hplbsh si necesita una estructura con fines comerciales.
Jake88

3
Los requisitos específicos de la variedad en los árboles es un argumento para tener diferentes tipos de árboles, para no tener ninguno.
André

50

La filosofía de STL es que elija un contenedor basado en garantías y no en cómo se implementa el contenedor. Por ejemplo, su elección de contenedor puede basarse en la necesidad de búsquedas rápidas. Por lo que a usted le importa, el contenedor puede implementarse como una lista unidireccional, siempre y cuando la búsqueda sea muy rápida, sería feliz. Eso es porque no estás tocando las partes internas de todos modos, estás usando iteradores o funciones miembro para el acceso. Su código no está vinculado a cómo se implementa el contenedor, sino a qué tan rápido es, o si tiene un orden fijo y definido, o si es eficiente en el espacio, y así sucesivamente.


12
No creo que esté hablando de implementaciones de contenedores, está hablando de un contenedor de árbol real.
Mooing Duck

3
@MooingDuck Creo que lo que significa wilhelmtell es que la biblioteca estándar de C ++ no define contenedores en función de su estructura de datos subyacente; solo define contenedores por su interfaz y características observables como el rendimiento asintótico. Cuando lo piensas, un árbol no es realmente un contenedor (como los conocemos). Ni siquiera tienen un a simple end()y begin()con el que puedes recorrer todos los elementos, etc.
Jordan Melo

77
@JordanMelo: Tonterías en todos los puntos. Es una cosa que contiene objetos. Es muy trivial diseñarlo para que tenga un iterador begin () y end () e iteradores bidireccionales para iterar. Cada contenedor tiene diferentes características. Sería útil si uno pudiera tener además características de árbol. Debería ser bastante fácil.
Mooing Duck

Por lo tanto, se desea tener un contenedor que proporcione búsquedas rápidas para los nodos secundarios y primarios, y requisitos razonables de memoria.
doc

@JordanMelo: Desde esa perspectiva, también los adaptadores como colas, pilas o colas prioritarias no pertenecerían al STL (tampoco tienen begin()y end()). Y recuerde que una cola prioritaria suele ser un montón, que al menos en teoría es un árbol (aunque las implementaciones reales). Por lo tanto, incluso si implementara un árbol como adaptador utilizando alguna estructura de datos subyacente diferente, sería elegible para ser incluido en el STL.
andreee

48

"Quiero almacenar una jerarquía de objetos como un árbol"

C ++ 11 vino y se fue y todavía no vieron la necesidad de proporcionar un std::tree, aunque la idea surgió (ver aquí ). Tal vez la razón por la que no han agregado esto es que es trivialmente fácil construir uno propio sobre los contenedores existentes. Por ejemplo...

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

Un recorrido simple usaría la recursividad ...

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

Si desea mantener una jerarquía y desea que funcione con algoritmos STL , las cosas pueden complicarse. Puede construir sus propios iteradores y lograr cierta compatibilidad, sin embargo, muchos de los algoritmos simplemente no tienen sentido para una jerarquía (cualquier cosa que cambie el orden de un rango, por ejemplo). Incluso definir un rango dentro de una jerarquía podría ser un negocio desordenado.


2
Si el proyecto puede permitir que se ordenen los hijos de un tree_node, entonces usar un std :: set <> en lugar del std :: vector <> y luego agregar un operador <() al objeto tree_node mejorará enormemente rendimiento de 'búsqueda' de un objeto similar a 'T'.
J Jorgenson

44
Resulta que eran flojos y realmente hicieron su primer ejemplo de Comportamiento indefinido.
user541686

2
@Mehrdad: Finalmente decidí pedir los detalles detrás de tu comentario aquí .
nobar

many of the algorithms simply don't make any sense for a hierarchy. Una cuestión de interpretación. Imagine una estructura de usuarios de stackoverflow y cada año desea que aquellos con una mayor cantidad de puntos de reputación lideren a aquellos con puntos de reputación más bajos. Proporcionando así un iterador BFS y una comparación adecuada, cada año que acaba de ejecutar std::sort(tree.begin(), tree.end()).
doc

Del mismo modo, podría construir fácilmente un árbol asociativo (para modelar registros de valores clave no estructurados, como JSON, por ejemplo) reemplazándolos vectorcon mapen el ejemplo anterior. Para un soporte completo de una estructura similar a JSON, puede usar variantpara definir los nodos.
nobar

43

Si está buscando una implementación de árbol RB, entonces stl_tree.h podría ser apropiado para usted también.


14
Curiosamente, esta es la única respuesta que realmente responde a la pregunta original.
Catskul

12
Considerando que quiere una "Heiarquía", parece seguro asumir que cualquier cosa con "equilibrio" es la respuesta incorrecta.
Mooing Duck

11
"Este es un archivo de encabezado interno, incluido por otros encabezados de biblioteca. No debe intentar usarlo directamente".
Dan

3
@Dan: Copiarlo no constituye usarlo directamente.
einpoklum

12

El mapa std :: se basa en un árbol negro rojo . También puede usar otros contenedores para ayudarlo a implementar sus propios tipos de árboles.


13
Usualmente usa árboles rojo-negros (no es necesario hacerlo).
Martin York

1
GCC usa un árbol para implementar el mapa. ¿Alguien quiere ver su directorio de inclusión de VC para ver qué usa Microsoft?
JJ

// Clase de árbol rojo-negro, diseñada para su uso en la implementación de STL // contenedores asociativos (set, multiset, map y multimap). Tomé eso de mi archivo stl_tree.h.
JJ

@JJ Al menos en Studio 2010, utiliza una ordered red-black tree of {key, mapped} values, unique keysclase interna , definida en <xtree>. No tengo acceso a una versión más moderna en este momento.
Justin Time - Restablece a Monica el

8

En cierto modo, std :: map es un árbol (se requiere que tenga las mismas características de rendimiento que un árbol binario equilibrado) pero no expone otra funcionalidad de árbol. El probable razonamiento detrás de no incluir una estructura de datos de árbol real probablemente fue solo una cuestión de no incluir todo en el stl. El stl puede verse como un marco para usar en la implementación de sus propios algoritmos y estructuras de datos.

En general, si hay una funcionalidad de biblioteca básica que desea, que no está en el stl, la solución es mirar BOOST .

De lo contrario, hay un montón de bibliotecas a cabo allí , dependiendo de las necesidades de su árbol.


6

Todos los contenedores STL se representan externamente como "secuencias" con un mecanismo de iteración. Los árboles no siguen este idioma.


77
Una estructura de datos de árbol podría proporcionar un recorrido previo, posterior o posterior a través de iteradores. De hecho, esto es lo que hace std :: map.
Andrew Tomazos

3
Sí y no ... depende de lo que quieras decir con "árbol". std::mapse implementa internamente como btree, pero externamente aparece como una SECUENCIA ordenada de PARES. Dado cualquier elemento que pueda preguntar universalmente quién es antes y quién es después. Una estructura de árbol general que contiene elementos, cada uno de los cuales contiene otros, no impone ningún orden o dirección. Puede definir iteradores que recorran una estructura de árbol de muchas maneras (sallow | deep first | last ...) pero una vez que lo hizo, un std::treecontenedor debe devolver uno de ellos de una beginfunción. Y no hay una razón obvia para devolver uno u otro.
Emilio Garavaglia

44
Un std :: map generalmente está representado por un árbol de búsqueda binario balanceado, no un árbol B. El mismo argumento que ha hecho podría aplicarse a std :: unordered_set, no tiene un orden natural, pero presenta iteradores de inicio y fin. El requisito de inicio y fin es solo que itera todos los elementos en un orden determinista, no que tiene que haber uno natural. preorder es un orden de iteración perfectamente válido para un árbol.
Andrew Tomazos

44
La implicación de su respuesta es que no hay una estructura de datos de árbol n stl porque no tiene una interfaz de "secuencia". Esto es simplemente incorrecto.
Andrew Tomazos

3
@EmiloGaravaglia: como lo demuestra std::unordered_set, que no tiene una "forma única" de iterar sus miembros (de hecho, el orden de iteración es pseudoaleatorio y la implementación está definida), pero sigue siendo un contenedor stl; esto refuta su punto. Iterar sobre cada elemento en un contenedor sigue siendo una operación útil, incluso si el orden no está definido.
Andrew Tomazos

4

Porque el STL no es una biblioteca "todo". Contiene, esencialmente, las estructuras mínimas necesarias para construir cosas.


13
Los árboles binarios son una funcionalidad extremadamente básica y, de hecho, más básica que otros contenedores, ya que tipos como std :: map, std :: multimap y stl :: set. Dado que esos tipos se basan en ellos, es de esperar que se exponga el tipo subyacente.
Catskul

2
No creo que el OP esté pidiendo un árbol binario , está pidiendo un árbol para almacenar una jerarquía.
Mooing Duck

No solo eso, agregar un "contenedor" de árbol a STL significaría agregar muchos conceptos nuevos, por ejemplo, un navegador de árbol (generalizador de iterador).
alfC

55
"Estructuras mínimas para construir cosas" es una declaración muy subjetiva. Puedes construir cosas con conceptos crudos de C ++, así que supongo que el mínimo verdadero no sería STL en absoluto.
doc


3

OMI, una omisión. Pero creo que hay buenas razones para no incluir una estructura de árbol en el STL. Hay mucha lógica en el mantenimiento de un árbol, que se escribe mejor como funciones miembro en el TreeNodeobjeto base . Cuando TreeNodeestá envuelto en un encabezado STL, simplemente se vuelve más desordenado.

Por ejemplo:

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); } 
} ;

77
Son muchos los punteros en bruto que tienes allí, muchos de los cuales no necesitan ser punteros en absoluto.
Mooing Duck

Te sugiero que retires esta respuesta. Una clase TreeNode es parte de una implementación de árbol.
einpoklum

3

Creo que hay varias razones por las cuales no hay árboles STL. Principalmente, los árboles son una forma de estructura de datos recursiva que, como un contenedor (lista, vector, conjunto), tiene una estructura fina muy diferente que dificulta las elecciones correctas. También son muy fáciles de construir en forma básica utilizando el STL.

Un árbol enraizado finito puede considerarse como un contenedor que tiene un valor o carga útil, digamos una instancia de una clase A y, una colección posiblemente vacía de (sub) árboles enraizados; Los árboles con una colección vacía de árboles se consideran hojas.

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
{};

Uno tiene que pensar un poco sobre el diseño del iterador, etc., y qué operaciones de productos y coproductos permite definir y ser eficientes entre árboles, y el STL original debe estar bien escrito, para que el conjunto vacío, el vector o el contenedor de la lista sea realmente vacío de cualquier carga útil en el caso predeterminado.

Los árboles juegan un papel esencial en muchas estructuras matemáticas (véanse los documentos clásicos de Butcher, Grossman y Larsen; también los documentos de Connes y Kriemer para ver ejemplos de cómo se pueden unir y cómo se usan para enumerar). No es correcto pensar que su papel es simplemente facilitar ciertas otras operaciones. Más bien facilitan esas tareas debido a su papel fundamental como estructura de datos.

Sin embargo, además de los árboles también hay "co-árboles"; los árboles sobre todo tienen la propiedad de que si eliminas la raíz, eliminas todo.

Considere los iteradores en el árbol, probablemente se realizarían como una simple pila de iteradores, a un nodo y a su padre, ... hasta la raíz.

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

Sin embargo, puedes tener tantos como quieras; colectivamente forman un "árbol" pero donde todas las flechas fluyen en la dirección hacia la raíz, este co-árbol puede iterarse a través de iteradores hacia el iterador trivial y la raíz; sin embargo, no se puede navegar a través o hacia abajo (no se conoce a los otros iteradores) ni se puede eliminar el conjunto de iteradores excepto si se realiza un seguimiento de todas las instancias.

Los árboles son increíblemente útiles, tienen mucha estructura, esto hace que sea un desafío serio obtener el enfoque definitivamente correcto. En mi opinión, es por eso que no se implementan en el STL. Además, en el pasado, he visto a personas volverse religiosas y encontrar desafiante la idea de un tipo de contenedor que contiene instancias de su propio tipo, pero tienen que enfrentarlo, eso es lo que representa un tipo de árbol: es un nodo que contiene un posiblemente colección vacía de árboles (más pequeños). El lenguaje actual lo permite sin problemas, ya que el constructor predeterminado container<B>no asigna espacio en el montón (ni en ningún otro lugar) para un B, etc.

Por mi parte, estaría satisfecho si esto, en buena forma, llegara a la norma.


0

Leyendo las respuestas aquí, las razones comunes nombradas son que uno no puede recorrer el árbol o que el árbol no asume la interfaz similar a otros contenedores STL y que uno no puede usar algoritmos STL con dicha estructura de árbol.

Teniendo eso en mente, traté de diseñar mi propia estructura de datos de árbol que proporcionará una interfaz similar a STL y será utilizable con los algoritmos STL existentes tanto como sea posible.

Mi idea era que el árbol debe basarse en los contenedores STL existentes y que no debe ocultar el contenedor, de modo que sea accesible para usar con algoritmos STL.

La otra característica importante que debe proporcionar el árbol son los iteradores de desplazamiento.

Esto es lo que pude encontrar: https://github.com/igagis/utki/blob/master/src/utki/tree.hpp

Y aquí están las pruebas: https://github.com/igagis/utki/blob/master/tests/tree/tests.cpp


-9

Todos los contenedores STL se pueden usar con iteradores. No puede tener un iterador y un árbol, porque no tiene una forma '' correcta '' de atravesar el árbol.


3
Pero puede decir que BFS o DFS es la forma correcta. O apoyar a los dos. O cualquier otro que puedas imaginar. Solo dígale al usuario qué es.
tomas789

2
en std :: map hay un iterador de árbol.
Jai

1
Un árbol podría definir su propio tipo de iterador personalizado que atraviese todos los nodos en orden de un "extremo" al otro (es decir, para cualquier árbol binario con las rutas 0 y 1, podría ofrecer un iterador que vaya de "todos los 0" a "todos" 1s "y un iterador inverso que hace lo contrario; para un árbol con una profundidad de 3 y nodo de inicio s, por ejemplo, se podría iterar sobre los nodos como s000, s00, s001, s0, s010, s01, s011, s, s100, s10, s101, s1, s110, s11, s111(" más a la izquierda" a 'más a la derecha'), sino que también podría utilizar un patrón de profundidad de recorrido ( s, s0, s1, s00, s01, s10, s11,
Justin Time - Restablece a Monica el

, etc.), o algún otro patrón, siempre que itere sobre cada nodo de tal manera que cada uno se pase solo una vez.
Justin Time - Restablece a Monica el

1
@doc, muy buen punto. Creo que std::unordered_setse "hizo" una secuencia porque no conocemos una mejor manera de iterar sobre los elementos que no sea una forma arbitraria (dada internamente por la función hash). Creo que es el caso opuesto del árbol: la iteración sobre unordered_setestá subespecificada, en teoría "no hay forma" de definir una iteración que no sea "al azar". En el caso del árbol hay muchas formas "buenas" (no aleatorias). Pero, nuevamente, su punto es válido.
alfC
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.