Perché C ++ STL non fornisce contenitori "ad albero"?


373

Perché il C ++ STL non fornisce alcun contenitore "albero", e qual è la cosa migliore da usare invece?

Voglio memorizzare una gerarchia di oggetti come un albero, piuttosto che usare un albero come miglioramento delle prestazioni ...


7
Ho bisogno di un albero per memorizzare una rappresentazione di una gerarchia.
Roddy,

20
Sono con il ragazzo che in fondo ha votato le risposte "corrette", che sembra essere; "Gli alberi sono inutili". Ci sono importanti se oscuri usi degli alberi.
Joe Soul-portatore

Penso che il motivo sia banale: nessuno lo ha ancora implementato nella libreria standard. È come se la libreria standard non avesse std::unordered_mape std::unordered_setfino a poco tempo fa. E prima ancora non c'erano contenitori STL nella libreria standard.
doc

1
I miei pensieri (non avendo mai letto lo standard pertinente, quindi questo è un commento, non una risposta) sono che l'STL non si preoccupa di specifiche strutture di dati, si preoccupa delle specifiche riguardanti la complessità e quali operazioni sono supportate. Pertanto, la struttura sottostante utilizzata può variare tra implementazioni e / o architetture di destinazione, purché soddisfi le specifiche. Sono abbastanza sicuro std::mape std::setuserò un albero in ogni implementazione là fuori, ma non devono farlo se una struttura non ad albero soddisfa anche le specifiche.
Mark K Cowan,

Risposte:


182

Esistono due motivi per cui potresti voler utilizzare un albero:

Volete rispecchiare il problema usando una struttura ad albero:
per questo abbiamo una libreria di grafici boost

Oppure vuoi un contenitore con caratteristiche di accesso ad albero. Per questo abbiamo

Fondamentalmente le caratteristiche di questi due contenitori sono tali che praticamente devono essere implementate usando alberi (sebbene questo non sia effettivamente un requisito).

Vedi anche questa domanda: implementazione dell'albero C.


64
Ci sono molte, molte ragioni per usare un albero, anche se questi sono i più comuni. Più comune! Uguale a tutti.
Joe Soul-portatore

3
Un terzo motivo importante per volere un albero è per un elenco sempre ordinato con inserimento / rimozione rapidi, ma per questo c'è std: multiset.
VoidStar,

1
@Durga: non sono sicuro di quanto la profondità sia rilevante quando si utilizza la mappa come contenitore ordinato. Mappa garantisce registro (n) inserimento / cancellazione / ricerca (e contenente elementi in ordine). Questa è la mappa per cui è usata ed è implementata (di solito) come un albero rosso / nero. Un albero rosso / nero si assicura che l'albero sia equilibrato. Quindi la profondità dell'albero è direttamente correlata al numero di elementi nell'albero.
Martin York,

14
Non sono d'accordo con questa risposta, sia nel 2008 che ora. La libreria standard non "ha" una spinta e la disponibilità di qualcosa in spinta non dovrebbe essere (e non è stata) una ragione per non adottarla nella norma. Inoltre, il BGL è generale e sufficientemente coinvolto da meritare classi di alberi specializzate indipendenti da esso. Inoltre, il fatto che std :: map e std :: set richiedono un albero è, IMO, un altro argomento per avere un stl::red_black_treeecc. Infine, gli alberi std::mape std::setsono bilanciati, un std::treepotrebbe non esserlo.
einpoklum,

1
@einpoklum: "la disponibilità di qualcosa in boost non dovrebbe essere un motivo per non adottarlo nello standard" - dato che uno degli scopi di boost è quello di fungere da banco di prova per utili librerie prima dell'incorporazione nello standard, posso solo dì "assolutamente!".
Martin Bonner supporta Monica il

94

Probabilmente per lo stesso motivo per cui non esiste un contenitore di alberi in boost. Esistono molti modi per implementare un tale contenitore e non esiste un buon modo per soddisfare tutti coloro che lo userebbero.

Alcuni problemi da considerare:

  • Il numero di figli per un nodo è fisso o variabile?
  • Quanto sovraccarico per nodo? - vale a dire, hai bisogno di puntatori genitore, puntatori di pari livello, ecc.
  • Quali algoritmi fornire? - diversi iteratori, algoritmi di ricerca, ecc.

Alla fine, il problema finisce per essere che un contenitore per alberi che sarebbe abbastanza utile per tutti, sarebbe troppo pesante per soddisfare la maggior parte delle persone che lo usano. Se stai cercando qualcosa di potente, Boost Graph Library è essenzialmente un superset di ciò per cui una libreria ad albero potrebbe essere utilizzata.

Ecco alcune altre implementazioni di alberi generici:


5
"... non è un buon modo per soddisfare tutti ..." Tranne il fatto che poiché stl :: map, stl :: multimap e stl :: set sono basati su rb_tree di stl, dovrebbe soddisfare tutti i casi di quei tipi base .
Catskul,

44
Considerando che non c'è modo di recuperare i figli di un nodo di un std::map, non chiamerei quei contenitori di alberi. Quelli sono contenitori associativi che sono comunemente implementati come alberi. Grande differenza.
Mooing Duck,

2
Sono d'accordo con Mooing Duck, come implementeresti una prima ricerca ampia su una std :: map? Sarà terribilmente costoso
Marco A.

1
Ho iniziato a utilizzare tree.hh di Kasper Peeters, tuttavia dopo aver esaminato le licenze per GPLv3 o qualsiasi altra versione di GPL, questo avrebbe contaminato il nostro software commerciale. Consiglierei di guardare gli alberi forniti nel commento di @hplbsh se hai bisogno di una struttura per scopi commerciali.
Jake88,

3
I requisiti specifici della varietà sugli alberi sono un argomento per avere diversi tipi di alberi, per non averne affatto.
André,

50

La filosofia della STL è quella di scegliere un container basato su garanzie e non sulla base dell'implementazione del container. Ad esempio, la scelta del contenitore potrebbe essere basata sulla necessità di ricerche rapide. Per quanto ti interessa, il contenitore può essere implementato come un elenco unidirezionale - fintanto che la ricerca è molto veloce sarai felice. Questo perché non tocchi comunque gli interni, stai usando iteratori o funzioni membro per l'accesso. Il tuo codice non è legato alla modalità di implementazione del contenitore, ma alla sua velocità, se ha un ordine fisso e definito, o se è efficiente nello spazio e così via.


12
Non penso che stia parlando di implementazioni di container, sta parlando di un vero e proprio container di alberi.
Mooing Duck,

3
@MooingDuck Penso che Wilhelmtell significhi che la libreria standard C ++ non definisce i contenitori in base alla struttura dei dati sottostante; definisce solo i contenitori dalla loro interfaccia e caratteristiche osservabili come le prestazioni asintotiche. Quando ci pensi, un albero non è affatto un contenitore (come li conosciamo). Non hanno nemmeno un diritto end()e begin()con il quale puoi scorrere tutti gli elementi, ecc.
Jordan Melo,

7
@JordanMelo: assurdità su tutti i punti. È una cosa che contiene oggetti. È molto banale progettarlo per avere un iteratore beg () e end () e bidirezionale con cui iterare. Ogni contenitore ha caratteristiche diverse. Sarebbe utile se si potessero avere anche altre caratteristiche dell'albero. Dovrebbe essere abbastanza facile.
Mooing Duck il

Pertanto, si desidera avere un contenitore che fornisca ricerche rapide per nodi figlio e padre e requisiti di memoria ragionevoli.
doc

@JordanMelo: Da quel punto di vista, anche adattatori come code, stack o code prioritarie non appartengono allo STL (anche loro non hanno begin()e end()). E ricorda che una coda prioritaria è in genere un heap, che almeno in teoria è un albero (anche se implementazioni effettive). Pertanto, anche se si implementasse un albero come un adattatore utilizzando una diversa struttura di dati sottostante, sarebbe idoneo a essere incluso nell'STL.
andreee

48

"Voglio memorizzare una gerarchia di oggetti come un albero"

Il C ++ 11 è venuto e se ne è andato e ancora non vedevano la necessità di fornire un std::tree, sebbene l'idea sia venuta fuori (vedi qui ). Forse il motivo per cui non l'hanno aggiunto è che è banalmente facile costruirne uno proprio sopra i contenitori esistenti. Per esempio...

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

Un semplice attraversamento avrebbe usato la ricorsione ...

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

Se vuoi mantenere una gerarchia e vuoi che funzioni con gli algoritmi STL , le cose potrebbero complicarsi. È possibile creare i propri iteratori e ottenere una certa compatibilità, tuttavia molti degli algoritmi semplicemente non hanno alcun senso per una gerarchia (tutto ciò che cambia l'ordine di un intervallo, ad esempio). Anche definire un intervallo all'interno di una gerarchia potrebbe essere un affare disordinato.


2
Se il progetto può consentire l'ordinamento dei figli di un tree_node, l'uso di uno std :: set <> al posto dello std :: vector <> e quindi l'aggiunta di un operatore <() all'oggetto tree_node migliorerà notevolmente performance di "ricerca" di un oggetto simile a "T".
J Jorgenson,

4
Si scopre che erano pigri e in realtà hanno reso il tuo primo esempio di comportamento indefinito.
user541686,

2
@Mehrdad: ho finalmente deciso di chiedere i dettagli dietro il tuo commento qui .
nobar,

many of the algorithms simply don't make any sense for a hierarchy. Una questione di interpretazione. Immagina una struttura di utenti stackoverflow e ogni anno desideri che quelli con un maggior numero di punti reputazione dominino quelli con punti reputazione inferiori. Fornendo così l'iteratore BFS e un confronto adeguato, ogni anno hai appena corso std::sort(tree.begin(), tree.end()).
doc

Allo stesso modo, potresti facilmente costruire un albero associativo (per modellare record di valori-chiave non strutturati, come ad esempio JSON) sostituendolo vectorcon mapnell'esempio sopra. Per il pieno supporto di una struttura simile a JSON, è possibile utilizzare variantper definire i nodi.
nobar,

43

Se stai cercando un'implementazione dell'albero RB, allora stl_tree.h potrebbe essere adatto anche a te.


14
Stranamente questa è l'unica risposta che risponde effettivamente alla domanda originale.
Catskul,

12
Considerando che vuole un "Heiarchy", sembra sicuro supporre che qualsiasi cosa con "bilanciamento" sia la risposta sbagliata.
Mooing Duck,

11
"Questo è un file di intestazione interno, incluso da altre intestazioni della libreria. Non dovresti tentare di usarlo direttamente."
Dan,

3
@Dan: copiarlo non significa utilizzarlo direttamente.
einpoklum,

12

la std :: map si basa su un albero nero rosso . Puoi anche utilizzare altri contenitori per aiutarti a implementare i tuoi tipi di alberi.


13
Di solito usa alberi rosso-neri (non è necessario farlo).
Martin York,

1
GCC utilizza un albero per implementare la mappa. Qualcuno vuole guardare la loro directory include VC per vedere cosa utilizza Microsoft?
JJ,

// Classe albero rosso-nero, progettata per l'uso nell'implementazione di contenitori associativi STL // (set, multiset, map e multimap). L'ho preso dal mio file stl_tree.h.
JJ,

@JJ Almeno in Studio 2010, utilizza una ordered red-black tree of {key, mapped} values, unique keysclasse interna , definita in <xtree>. Non ho accesso a una versione più moderna al momento.
Justin Time - Ripristina Monica il

8

In un certo senso, std :: map è un albero (è necessario che abbia le stesse caratteristiche prestazionali di un albero binario bilanciato) ma non espone altre funzionalità dell'albero. Il probabile ragionamento alla base del non includere una struttura di dati ad albero reale era probabilmente solo una questione di non includere tutto nello stl. Lo stl può essere considerato come un framework da utilizzare per implementare i propri algoritmi e strutture dati.

In generale, se c'è una funzionalità di libreria di base che si desidera, che non è nello stl, la correzione è guardare BOOST .

Altrimenti, ci sono un sacco di librerie fuori , a seconda delle esigenze del tuo albero.


6

Tutti i contenitori STL sono rappresentati esternamente come "sequenze" con un meccanismo di iterazione. Gli alberi non seguono questo linguaggio.


7
Una struttura di dati ad albero potrebbe fornire l'attraversamento del preordine, inorder o postorder tramite iteratori. In effetti questo è ciò che fa std :: map.
Andrew Tomazos,

3
Sì e no ... dipende da cosa intendi per "albero". std::mapè implementato internamente come btree, ma esternamente appare come una SEQUENZA ordinata di COPPIE. Dato qualunque elemento tu possa chiedere universalmente chi è prima e chi è dopo. Una struttura ad albero generale contenente elementi ciascuno dei quali contiene altri non impone alcun ordinamento o direzione. È possibile definire gli iteratori che percorrono una struttura ad albero in molti modi (sallow | deep first | last ...) ma una volta che lo hai fatto, un std::treecontenitore deve restituirne uno da una beginfunzione. E non vi è alcuna ragione ovvia per restituire l'uno o l'altro.
Emilio Garavaglia,

4
Una std :: map è generalmente rappresentata da un albero di ricerca binario bilanciato, non da un albero B. Lo stesso argomento che hai fatto potrebbe essere applicato a std :: unordered_set, non ha un ordine naturale, eppure presenta iteratori di inizio e fine. Il requisito di inizio e fine è solo che itera tutti gli elementi in un ordine deterministico, non che ce ne debba essere uno naturale. il preordine è un ordine di iterazione perfettamente valido per un albero.
Andrew Tomazos,

4
L'implicazione della tua risposta è che non esiste una struttura dati stl n-tree perché non ha un'interfaccia "sequenza". Questo è semplicemente errato.
Andrew Tomazos,

3
@EmiloGaravaglia: come evidenziato da std::unordered_set, che non ha "un modo unico" di iterare i suoi membri (in effetti l'ordine di iterazione è pseudo-casuale e l'implementazione definita), ma è ancora un contenitore stl - questo confuta il tuo punto. L'iterazione su ciascun elemento in un contenitore è ancora un'operazione utile, anche se l'ordine non è definito.
Andrew Tomazos,

4

Perché STL non è una libreria "tutto". Contiene essenzialmente le strutture minime necessarie per costruire le cose.


13
Gli alberi binari sono una funzionalità estremamente semplice e, in effetti, più basilari di altri contenitori poiché tipi come std :: map, std :: multimap e stl :: set. Poiché tali tipi si basano su di essi, ti aspetteresti che il tipo sottostante sia esposto.
Catskul,

2
Non credo che l'OP stia chiedendo un albero binario , sta chiedendo un albero per memorizzare una gerarchia.
Mooing Duck,

Non solo, l'aggiunta di un "contenitore" ad albero a STL avrebbe comportato l'aggiunta di molti nuovi concetti, ad esempio un navigatore ad albero (generalizzando Iterator).
alfC

5
"Strutture minime per costruire le cose" è un'affermazione molto soggettiva. Puoi costruire cose con concetti C ++ non elaborati, quindi credo che il minimo vero non sarebbe affatto STL.
doc


3

IMO, un'omissione. Ma penso che ci siano buone ragioni per non includere una struttura ad albero nell'STL. C'è molta logica nel mantenere un albero, che è meglio scritto come funzioni membro TreeNodenell'oggetto base . Quando TreeNodeè racchiuso in un'intestazione STL, diventa solo più incasinato.

Per esempio:

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
È un sacco di possedere puntatori grezzi che hai lì, molti dei quali non hanno bisogno di essere affatto puntatori.
Mooing Duck,

Ti suggerisco di ritirare questa risposta. Una classe TreeNode fa parte di un'implementazione ad albero.
einpoklum,

3

Penso che ci siano diverse ragioni per cui non ci sono alberi STL. Principalmente gli alberi sono una forma di struttura di dati ricorsiva che, come un contenitore (elenco, vettore, set), ha una struttura fine molto diversa che rende difficili le scelte corrette. Sono anche molto facili da costruire in forma base usando l'STL.

Un albero con radice finita può essere pensato come un contenitore che ha un valore o un payload, ad esempio un'istanza di una classe A e, eventualmente, una raccolta vuota di (radici) alberi con radici; gli alberi con raccolta di sottotitoli vuoti sono considerati foglie.

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

Bisogna pensare un po 'al design dell'iteratore ecc. E quali operazioni di prodotto e coprodotto si consentono di definire ed essere efficienti tra gli alberi - e l'STL originale deve essere ben scritto - in modo che il contenitore vuoto di set, vettore o elenco sia veramente vuoto di qualsiasi payload nel caso predefinito.

Gli alberi svolgono un ruolo essenziale in molte strutture matematiche (vedere gli articoli classici di Butcher, Grossman e Larsen; anche gli articoli di Connes e Kriemer per esempi di essi possono essere uniti e come vengono utilizzati per elencare). Non è corretto pensare che il loro ruolo sia semplicemente quello di facilitare alcune altre operazioni. Piuttosto facilitano tali compiti a causa del loro ruolo fondamentale come struttura di dati.

Tuttavia, oltre agli alberi ci sono anche "co-alberi"; gli alberi hanno soprattutto la proprietà che se si elimina la radice si elimina tutto.

Considera gli iteratori sull'albero, probabilmente verrebbero realizzati come una semplice pila di iteratori, su un nodo e sul suo genitore, ... fino alla radice.

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

Tuttavia, puoi averne quanti ne desideri; collettivamente formano un "albero" ma dove tutte le frecce scorrono nella direzione verso la radice, questo co-albero può essere ripetuto attraverso iteratori verso il banale iteratore e radice; tuttavia non può essere navigato attraverso o verso il basso (gli altri iteratori non gli sono noti) né è possibile eliminare l'insieme di iteratori se non tenendo traccia di tutte le istanze.

Gli alberi sono incredibilmente utili, hanno molta struttura, questo rende una sfida seria ottenere l'approccio definitivamente corretto. A mio avviso, questo è il motivo per cui non sono implementati nell'STL. Inoltre, in passato, ho visto persone diventare religiose e trovare stimolante l'idea di un tipo di contenitore contenente istanze del proprio tipo - ma devono affrontarlo - questo è ciò che rappresenta un tipo di albero - è un nodo che contiene un raccolta forse vuota di alberi (più piccoli). La lingua corrente lo consente senza difficoltà fornendo il costruttore predefinito per container<B>non allocare spazio sull'heap (o in qualsiasi altro luogo) per un B, ecc.

Per quanto mi riguarda sarei contento se questo, in una buona forma, trovasse la sua strada nello standard.


0

Leggendo le risposte qui, i motivi comuni citati sono che non si può iterare attraverso l'albero o che l'albero non assume l'interfaccia simile ad altri contenitori STL e non si possono usare algoritmi STL con tale struttura ad albero.

Tenendo presente ciò, ho cercato di progettare la mia struttura di dati ad albero che fornirà un'interfaccia simile a STL e sarà utilizzabile il più possibile con algoritmi STL esistenti.

La mia idea era che l'albero deve essere basato sui contenitori STL esistenti e che non deve nascondere il contenitore, in modo che sia accessibile da usare con gli algoritmi STL.

L'altra caratteristica importante che l'albero deve fornire sono gli iteratori che attraversano.

Ecco cosa sono riuscito a inventare: https://github.com/igagis/utki/blob/master/src/utki/tree.hpp

Ed ecco i test: https://github.com/igagis/utki/blob/master/tests/tree/tests.cpp


-9

Tutti i contenitori STL possono essere utilizzati con iteratori. Non puoi avere un iteratore e un albero, perché non hai un modo "giusto" per attraversare l'albero.


3
Ma puoi dire che BFS o DFS è il modo corretto. O supportali entrambi. O qualsiasi altro tu possa immaginare. Basta dire all'utente di cosa si tratta.
tomas789,

2
in std :: map c'è iteratore dell'albero.
Jai,

1
Un albero potrebbe definire il proprio tipo di iteratore personalizzato che attraversa tutti i nodi in ordine da un "estremo" all'altro (ovvero per qualsiasi albero binario con percorsi 0 e 1, potrebbe offrire un iteratore che va da "tutti gli 0" a "tutto 1s "e un iteratore inverso che fa l'opposto, per un albero con una profondità di 3 e nodo di partenza s, per esempio, potrebbe iterare i nodi come s000, s00, s001, s0, s010, s01, s011, s, s100, s10, s101, s1, s110, s11, s111(" sinistra" a 'destra'), ma potrebbe anche utilizzare un modello di profondità attraversamento ( s, s0, s1, s00, s01, s10, s11,
Justin Time - Ripristina Monica il

, ecc.) o qualche altro modello, purché iteri su ogni nodo in modo tale che ciascuno venga passato una sola volta.
Justin Time - Ripristina Monica il

1
@doc, ottimo punto. Penso che sia std::unordered_setstata "fatta" una sequenza perché non conosciamo un modo migliore di iterare su elementi diversi da un modo arbitrario (dato internamente dalla funzione hash). Penso che sia il caso opposto dell'albero: l'iterazione sopra unordered_setè sotto specificata, in teoria non c'è "modo" di definire un'iterazione se non forse "casualmente". Nel caso dell'albero ci sono molti modi "buoni" (non casuali). Ma, ancora una volta, il tuo punto è valido.
alfC
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.