Por que o C ++ STL não fornece nenhum contêiner "em árvore"?


373

Por que o C ++ STL não fornece nenhum contêiner de "árvore" e qual é a melhor coisa a ser usada?

Quero armazenar uma hierarquia de objetos como uma árvore, em vez de usá-la como um aprimoramento de desempenho ...


7
Eu preciso de uma árvore para armazenar uma representação de uma hierarquia.
Roddy

20
Estou com o cara que votou negativamente nas respostas "corretas", o que parece ser; "Árvores são inúteis". Existem usos importantes, se obscuros, das árvores.
31510 Joe Soul-bringer

Eu acho que o motivo é trivial - ninguém o implementou na biblioteca padrão ainda. É como se a biblioteca padrão não tivesse std::unordered_mape std::unordered_setaté recentemente. E antes disso, não havia contêineres STL na biblioteca padrão.
doc

11
Meu pensamento (nunca tendo lido o padrão relevante, portanto, este é um comentário, não uma resposta) é que o STL não se importa com estruturas de dados específicas, se preocupa com especificações relacionadas à complexidade e com quais operações são suportadas. Portanto, a estrutura subjacente usada pode variar entre implementações e / ou arquiteturas de destino, desde que atenda às especificações. Tenho certeza std::mape std::setirá utilizar uma árvore em todas as implementações lá fora, mas eles não têm que se se alguma estrutura não-árvore também atende às especificações.
MarkK Cowan

Respostas:


182

Há duas razões pelas quais você pode querer usar uma árvore:

Você deseja espelhar o problema usando uma estrutura semelhante a uma árvore:
Para isso, temos a biblioteca de gráficos boost

Ou você quer um contêiner que tenha características de acesso semelhantes a árvores. Para isso, temos

Basicamente, as características desses dois contêineres são tais que praticamente precisam ser implementadas usando árvores (embora isso não seja realmente um requisito).

Veja também esta pergunta: Implementação da árvore C


64
Existem muitas razões para usar uma árvore, mesmo que estas sejam as mais comuns. Mais comum! Igual a todos.
Joe Soul-bringer

3
Um terceiro principal motivo para querer uma árvore é para uma lista sempre classificada com inserção / remoção rápida, mas para isso existe std: multiset.
VoidStar

11
@ Durga: Não tenho certeza de como a profundidade é relevante quando você está usando o mapa como um contêiner classificado. O mapa garante a inserção / exclusão / pesquisa de log (n) (e contendo elementos na ordem de classificação). É para isso que o mapa é usado e implementado (geralmente) como uma árvore vermelha / preta. Uma árvore vermelha / preta garante que a árvore esteja equilibrada. Portanto, a profundidade da árvore está diretamente relacionada ao número de elementos na árvore.
Martin York

14
Não concordo com esta resposta, tanto em 2008 como agora. A biblioteca padrão "não possui" aumento e a disponibilidade de algo no aumento não deve ser (e não foi) um motivo para não adotá-lo no padrão. Além disso, o BGL é geral e envolvido o suficiente para merecer classes de árvore especializadas independentes dele. Além disso, o fato de que std :: map e std :: set requerem uma árvore é, IMO, outro argumento para ter um stl::red_black_treeetc. Finalmente, as árvores std::mape std::setsão equilibradas, e std::treepode não ser.
Einpoklum 26/07/16

11
@einpoklum: "a disponibilidade de algo no impulso não deve ser uma razão para não adotá-lo no padrão" - dado que um dos objetivos do impulso é atuar como um campo de prova para bibliotecas úteis antes da incorporação no padrão, só posso diga "absolutamente!".
Martin Bonner apoia Monica

94

Provavelmente pela mesma razão que não há um contêiner de árvore no impulso. Existem várias maneiras de implementar esse contêiner e não há uma boa maneira de satisfazer todos que o usariam.

Algumas questões a serem consideradas:

  • O número de filhos de um nó é fixo ou variável?
  • Quanta sobrecarga por nó? - ou seja, você precisa de ponteiros pai, ponteiros irmãos, etc.
  • Quais algoritmos fornecer? - diferentes iteradores, algoritmos de pesquisa, etc.

No final, o problema acaba sendo que um contêiner de árvore que seria útil o suficiente para todos seria muito pesado para satisfazer a maioria das pessoas que o usavam. Se você está procurando algo poderoso, a Boost Graph Library é essencialmente um superconjunto do que uma biblioteca em árvore pode ser usada.

Aqui estão algumas outras implementações genéricas de árvores:


5
"... não existe uma boa maneira de satisfazer a todos ..." Exceto que, como stl :: map, stl :: multimap e stl :: set são baseados no rb_tree do stl, ele deve atender a tantos casos quanto esses tipos básicos .
Catskul

44
Considerando que não há como recuperar os filhos de um nó de a std::map, eu não chamaria esses recipientes de árvore. Esses são contêineres associativos que geralmente são implementados como árvores. Grande diferença.
Mooing Duck

2
Concordo com o Mooing Duck, como você implementaria uma primeira pesquisa abrangente em um std :: map? Vai ser muito caro
Marco A.

11
Comecei a usar o tree.hh do Kasper Peeters. No entanto, depois de revisar o licenciamento da GPLv3 ou de qualquer outra versão da GPL, ela contaminaria nosso software comercial. Eu recomendaria olhar a árvore fornecida no comentário por @hplbsh se você precisar de uma estrutura para fins comerciais.
Jake88

3
Os requisitos específicos de variedade para as árvores são um argumento para ter diferentes tipos de árvores, para não ter nenhuma.
André

50

A filosofia da STL é que você escolha um contêiner com base em garantias e não em como o contêiner é implementado. Por exemplo, sua escolha de contêiner pode se basear na necessidade de pesquisas rápidas. Para todos os cuidados, o contêiner pode ser implementado como uma lista unidirecional - desde que a pesquisa seja muito rápida, você ficará feliz. Isso porque você não está tocando os internos de qualquer maneira, está usando iteradores ou funções de membro para o acesso. Seu código não está vinculado à maneira como o contêiner é implementado, mas à rapidez com que ele é, ou se possui uma ordem fixa e definida, ou se é eficiente no espaço e assim por diante.


12
Eu não acho que ele está falando sobre implementações de contêiner, ele está falando sobre um contêiner de árvore real.
Mooing Duck

3
@MooingDuck Acho que o que wilhelmtell significa é que a biblioteca padrão C ++ não define contêineres com base em sua estrutura de dados subjacente; apenas define contêineres por sua interface e características observáveis, como desempenho assintótico. Quando você pensa sobre isso, uma árvore não é realmente um contêiner (como os conhecemos). Eles nem sequer têm um aa para a frente end()e begin()com os quais você pode iterar por todos os elementos etc.
Jordan Melo

7
@ JordanMelo: Bobagem em todos os pontos. É uma coisa que contém objetos. É muito trivial projetá-lo para ter iteradores begin () e end () e bidirecionais para iterar. Cada contêiner possui características diferentes. Seria útil se alguém pudesse adicionalmente ter características de árvore. Deve ser bem fácil.
Mooing Duck

Assim, deseja-se ter um contêiner que forneça pesquisas rápidas para nós filhos e pais, além de requisitos razoáveis ​​de memória.
doc

@ JordanMelo: Nessa perspectiva, também adaptadores como filas, pilhas ou filas de prioridade não pertenceriam ao STL (eles também não possuem begin()e end()). E lembre-se de que uma fila de prioridade normalmente é uma pilha, que pelo menos em teoria é uma árvore (mesmo que implementações reais). Portanto, mesmo se você implementasse uma árvore como um adaptador usando alguma estrutura de dados subjacente diferente, seria elegível para ser incluído no STL.
andreee

48

"Eu quero armazenar uma hierarquia de objetos como uma árvore"

O C ++ 11 veio e se foi e eles ainda não viram a necessidade de fornecer um std::tree, embora a idéia tenha surgido (veja aqui ). Talvez a razão pela qual eles não tenham adicionado isso seja o fato de ser trivialmente fácil criar o seu próprio sobre os contêineres existentes. Por exemplo...

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

Uma travessia simples usaria recursão ...

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

Se você deseja manter uma hierarquia e trabalhar com algoritmos STL , as coisas podem ficar complicadas. Você pode criar seus próprios iteradores e obter alguma compatibilidade, no entanto, muitos dos algoritmos simplesmente não fazem sentido para uma hierarquia (qualquer coisa que mude a ordem de um intervalo, por exemplo). Mesmo definir um intervalo dentro de uma hierarquia pode ser um negócio confuso.


2
Se o projeto permitir que os filhos de um tree_node sejam classificados, o uso de um std :: set <> no lugar do std :: vector <> e a adição de um operador <() ao objeto tree_node melhorará bastante 'pesquisar' o desempenho de um objeto do tipo 'T'.
JJorgenson #

4
Acontece que eles eram preguiçosos e realmente fizeram seu primeiro exemplo de comportamento indefinido.
user541686

2
@ Mehrdad: Eu finalmente decidi pedir os detalhes por trás do seu comentário aqui .
Nobar

many of the algorithms simply don't make any sense for a hierarchy. Uma questão de interpretação. Imagine uma estrutura de usuários de stackoverflow e, a cada ano, você deseja que aqueles com maior quantidade de pontos de reputação controlem aqueles com menos pontos de reputação. Fornecendo, assim, o iterador BFS e a comparação apropriada, todos os anos que você apenas executa std::sort(tree.begin(), tree.end()).
doc

Da mesma forma, você pode criar facilmente uma árvore associativa (para modelar registros de valor-chave não estruturados, como JSON, por exemplo), substituindo vectorpor mapno exemplo acima. Para suporte total de uma estrutura semelhante a JSON, você pode usar variantpara definir os nós.
Nobar

43

Se você está procurando uma implementação de árvore RB, stl_tree.h também pode ser apropriado para você.


14
Estranhamente, esta é a única resposta que realmente responde à pergunta original.
Catskul

12
Considerando que ele quer uma "heiarquia", parece seguro assumir que qualquer coisa com "equilíbrio" é a resposta errada.
Mooing Duck

11
"Este é um arquivo de cabeçalho interno, incluído por outros cabeçalhos de biblioteca. Você não deve tentar usá-lo diretamente."
23415 Dan

3
@ Dan: Copiá-lo não constitui usá-lo diretamente.
Einpoklum

12

o std :: map é baseado em uma árvore vermelha e preta . Você também pode usar outros contêineres para ajudá-lo a implementar seus próprios tipos de árvores.


13
Geralmente usa árvores vermelho-pretas (não é necessário fazê-lo).
Martin York

11
O GCC usa uma árvore para implementar o mapa. Alguém quer olhar para o seu diretório de inclusão VC para ver o que a Microsoft usa?
JJ

// Classe de árvore vermelho-preto, projetada para uso na implementação de contêineres associativos STL // (conjunto, multiset, mapa e multimap). Peguei isso do meu arquivo stl_tree.h.
JJ

@JJ Pelo menos no Studio 2010, ele usa uma ordered red-black tree of {key, mapped} values, unique keysclasse interna , definida em <xtree>. Não tenha acesso a uma versão mais moderna no momento.
Justin Time - Restabelece Monica

8

De certa forma, std :: map é uma árvore (é necessário ter as mesmas características de desempenho que uma árvore binária balanceada), mas não expõe outras funcionalidades da árvore. O provável raciocínio por trás da não inclusão de uma estrutura de dados em árvore real provavelmente foi apenas uma questão de não incluir tudo no stl. O stl pode ser visto como uma estrutura a ser usada na implementação de seus próprios algoritmos e estruturas de dados.

Em geral, se você deseja uma funcionalidade básica da biblioteca, que não esteja no stl, a correção é procurar o BOOST .

Caso contrário, há um monte de bibliotecas para fora , dependendo das necessidades de sua árvore.


6

Todos os contêineres STL são representados externamente como "sequências" com um mecanismo de iteração. As árvores não seguem esse idioma.


7
Uma estrutura de dados em árvore pode fornecer passagem de pré-encomenda, entrada ou pós-encomenda através de iteradores. De fato, é isso que o std :: map faz.
Andrew Tomazos

3
Sim e não ... depende do que você quer dizer com "árvore". std::mapé implementado internamente como btree, mas externamente aparece como uma SEQUÊNCIA ordenada de pares. Dado qualquer elemento, você pode perguntar universalmente quem é antes e quem é depois. Uma estrutura geral de árvore que contém elementos, cada um dos quais contém outros, não impõe nenhuma classificação ou direção. Você pode definir iteradores que percorrem uma estrutura de árvore de várias maneiras (sallow | deep first | last ...), mas depois que você fez isso, um std::treecontainer deve retornar um deles de uma beginfunção. E não há razão óbvia para retornar um ou outro.
Emilio Garavaglia

4
Um std :: map geralmente é representado por uma árvore de pesquisa binária equilibrada, não por uma árvore B. O mesmo argumento que você formulou pode se aplicar ao std :: unordered_set, ele não tem ordem natural, mas apresenta iteradores de início e fim. O requisito de início e fim é apenas que itera todos os elementos em alguma ordem determinística, não que tenha que haver um natural. pré-encomenda é uma ordem de iteração perfeitamente válida para uma árvore.
Andrew Tomazos

4
A implicação da sua resposta é que não há estrutura de dados stl n-tree porque ela não possui uma interface de "sequência". Isto é simplesmente incorreto.
Andrew Tomazos

3
@EmiloGaravaglia: Como evidenciado por std::unordered_set, que não tem uma "maneira única" de iterar seus membros (na verdade, a ordem de iteração é pseudo-aleatória e a implementação é definida), mas ainda é um contêiner stl - isso desmente o seu ponto. A iteração sobre cada elemento em um contêiner ainda é uma operação útil, mesmo que a ordem seja indefinida.
Andrew Tomazos

4

Porque o STL não é uma biblioteca "tudo". Ele contém, essencialmente, as estruturas mínimas necessárias para construir coisas.


13
As árvores binárias são uma funcionalidade extremamente básica e, de fato, mais básica do que outros contêineres, pois tipos como std :: map, std :: multimap e stl :: set. Como esses tipos são baseados neles, você esperaria que o tipo subjacente fosse exposto.
Catskul

2
Eu não acho que o OP esteja pedindo uma árvore binária , ele está pedindo uma árvore para armazenar uma hierarquia.
Mooing Duck

Não apenas isso, adicionar um "contêiner" de árvore ao STL significaria adicionar muitos conceitos novos, por exemplo, um navegador de árvore (generalizador do Iterator).
AlfC 17/08/16

5
"Estruturas mínimas para construir coisas" é uma afirmação muito subjetiva. Você pode criar coisas com conceitos brutos de C ++, então eu acho que o mínimo verdadeiro não seria STL.
doc


3

OMI, uma omissão. Mas acho que há boas razões para não incluir uma estrutura em árvore no STL. Há muita lógica na manutenção de uma árvore, que é melhor gravada como funções de membro no TreeNodeobjeto base . Quando TreeNodeé embrulhado em um cabeçalho STL, fica mais confuso.

Por exemplo:

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

7
São muitos os ponteiros brutos que você possui, muitos dos quais não precisam ser ponteiros.
Mooing Duck

Sugira que você retire esta resposta. Uma classe TreeNode faz parte de uma implementação em árvore.
Einpoklum 26/07/16

3

Eu acho que existem várias razões pelas quais não existem árvores STL. Principalmente Árvores são uma forma de estrutura de dados recursiva que, como um contêiner (lista, vetor, conjunto), possui uma estrutura fina muito diferente, o que torna as escolhas corretas complicadas. Eles também são muito fáceis de construir de forma básica usando o STL.

Uma árvore enraizada finita pode ser vista como um contêiner que possui um valor ou carga útil, por exemplo, uma instância de uma classe A e uma coleção possivelmente vazia de (sub) árvores enraizadas; as árvores com uma coleção vazia de subárvores são consideradas folhas.

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

É preciso pensar um pouco sobre o design do iterador etc. e quais operações de produto e coproduto se permite definir e ser eficiente entre árvores - e o STL original deve ser bem escrito - para que o conjunto vazio, o vetor ou o contêiner de lista seja realmente vazio de qualquer carga útil no caso padrão.

As árvores desempenham um papel essencial em muitas estruturas matemáticas (veja os artigos clássicos de Butcher, Grossman e Larsen; também os artigos de Connes e Kriemer, para exemplos de como podem ser unidos e como são usados ​​para enumerar). Não é correto pensar que o papel deles é simplesmente facilitar outras operações. Em vez disso, eles facilitam essas tarefas devido ao seu papel fundamental como estrutura de dados.

No entanto, além das árvores, também existem "co-árvores"; acima de tudo, as árvores têm a propriedade de que, se você excluir a raiz, excluirá tudo.

Considere os iteradores na árvore, provavelmente eles seriam realizados como uma simples pilha de iteradores, para um nó e seu pai, ... até a raiz.

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

No entanto, você pode ter quantos quiser; coletivamente, formam uma "árvore", mas onde todas as setas fluem na direção da raiz, essa co-árvore pode ser iterada através de iteradores em direção ao iterador e raiz triviais; no entanto, ele não pode ser navegado entre ou para baixo (os outros iteradores não são conhecidos por ele) nem o conjunto de iteradores pode ser excluído, exceto acompanhando todas as instâncias.

As árvores são incrivelmente úteis, possuem muita estrutura, o que torna um sério desafio obter a abordagem definitivamente correta. Na minha opinião, é por isso que eles não são implementados no STL. Além disso, no passado, eu vi pessoas se tornarem religiosas e encontrar a idéia de um tipo de contêiner contendo instâncias de seu próprio tipo desafiador - mas elas precisam encarar - é isso que um tipo de árvore representa - é um nó que contém um coleção possivelmente vazia de árvores (menores). O idioma atual permite isso sem desafio, desde que o construtor padrão container<B>não aloque espaço no heap (ou em qualquer outro lugar) para um B, etc.

Eu ficaria satisfeito se isso, em boa forma, encontrasse seu caminho no padrão.


0

Lendo as respostas aqui, os motivos comuns mencionados são que não se pode iterar pela árvore ou que a árvore não assume a interface semelhante a outros contêineres STL e não se pode usar algoritmos STL com essa estrutura em árvore.

Tendo isso em mente, tentei projetar minha própria estrutura de dados em árvore, que fornecerá uma interface semelhante a STL e será utilizável com os valores de STL existentes o máximo possível.

Minha idéia foi que a árvore deve ser baseada nos contêineres STL existentes e que não oculte o contêiner, para que seja acessível para uso com os algoritmos STL.

O outro recurso importante que a árvore deve fornecer são os iteradores que atravessam.

Aqui está o que eu consegui: https://github.com/igagis/utki/blob/master/src/utki/tree.hpp

E aqui estão os testes: https://github.com/igagis/utki/blob/master/tests/tree/tests.cpp


-9

Todos os contêineres STL podem ser usados ​​com iteradores. Você não pode ter um iterador e uma árvore, porque você não tem um caminho 'certo' para atravessar a árvore.


3
Mas você pode dizer que BFS ou DFS é a maneira correta. Ou apoie os dois. Ou qualquer outro que você possa imaginar. Basta dizer ao usuário o que é.
precisa saber é o seguinte

2
no std :: map existe o iterador da árvore.
Jai

11
Uma árvore pode definir seu próprio tipo de iterador personalizado que atravessa todos os nós, de um "extremo" ao outro (ou seja, para qualquer árvore binária com os caminhos 0 e 1, poderia oferecer um iterador que passa de "todos os 0s" para "todos" 1s "e uma iteração inversa que faz o contrário; para uma árvore com uma profundidade de 3 e nó de partida s, por exemplo, poderia interagir sobre os nós como s000, s00, s001, s0, s010, s01, s011, s, s100, s10, s101, s1, s110, s11, s111(" mais à esquerda" para 'mais à direita'), mas também pode usar um padrão de profundidade travessia ( s, s0, s1, s00, s01, s10, s11,
Justin Time - Restabelecer Monica

, etc.) ou algum outro padrão, desde que itere sobre todos os nós, de forma que cada um seja passado apenas uma única vez.
Justin Time - Restabelece Monica

11
@ doc, muito bom ponto. Eu acho que std::unordered_setfoi "feito" uma sequência porque não conhecemos uma maneira melhor de iterar sobre os elementos além de alguma maneira arbitrária (fornecida internamente pela função hash). Eu acho que é o caso oposto da árvore: a iteração acima unordered_seté subespecificada, em teoria não há "maneira" de definir uma iteração que não seja "aleatoriamente". No caso da árvore, existem muitas maneiras "boas" (não aleatórias). Mas, novamente, seu argumento é válido.
AlfC30
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.