为什么C++ STL不提供任何"树"容器?

Rod*_*ddy 362 c++ tree stl

为什么C++ STL不提供任何"树"容器,而最好使用什么?

我想将对象的层次结构存储为树,而不是使用树作为性能增强...

Mar*_*ork 177

您可能想要使用树有两个原因:

您希望使用类似树的结构来镜像问题:
为此,我们使用了boost图库

或者你想要一个具有树状访问特征的容器为此我们有

基本上这两个容器的特征是它们实际上必须使用树来实现(尽管这实际上不是必需的).

另见这个问题: C树实现

  • 使用树的原因有很多,即使这些是最常见的.最常见的!等于全部. (56认同)
  • 我在2008年和现在都不同意这个答案.标准库没有"有"提升,并且提升中某些东西的可用性不应该(并且还没有)成为不将其纳入标准的理由.此外,BGL是通用的,并且足以支持独立于其的专用树类.另外,std :: map和std :: set需要树的事实是,IMO,另一个参数是拥有`stl :: red_black_tree`等等.最后,`std :: map`和`std :: set`树是平衡的,`std :: tree`可能不是. (13认同)
  • 请将std :: multiset和std :: multimap添加到列表中. (6认同)
  • @einpoklum:“boost 中某些内容的可用性不应成为不将其纳入标准的理由” - 鉴于 boost 的“目的”之一是在纳入标准之前充当有用库的试验场,我只能说“绝对!”。 (6认同)
  • 想要树的第三个主要原因是具有快速插入/删除的始终排序列表,但是为此存在std:multiset. (3认同)

Gre*_*ers 92

可能出于同样的原因,在增强中没有树容器.有很多方法可以实现这样一个容器,并没有很好的方法来满足每个使用它的人.

需要考虑的一些问题:
- 节点的子节点数是固定的还是可变的?
- 每个节点有多少开销?- 即,你需要父指针,兄弟指针等
- 提供什么算法? - 不同的迭代器,搜索算法等

最后,问题最终是一个对每个人都足够有用的树容器,对于大多数使用它的人来说太重了.如果您正在寻找功能强大的东西,Boost Graph Library本质上是树库可用的超集.

以下是一些其他通用树实现:
- Kasper Peeters的tree.hh
- Adobe的林
- 核心::树

  • 考虑到无法检索`std :: map`节点的子节点,我不会调用那些树容器.这些是通常作为树实现的关联容器.很大的区别. (41认同)
  • "......没有好办法让每个人都满意......"除了因为stl :: map,stl :: multimap和stl :: set都是基于stl的rb_tree,它应该满足那些基本类型的情况. . (4认同)
  • 对树木的特定品种要求是拥有不同类型的树木而不是根本没有树木的论点。 (3认同)
  • 我同意 Mooing Duck,你将如何在 std::map 上实现广度优先搜索?它会非常昂贵 (2认同)

wil*_*ell 50

STL的理念是您选择基于保证的容器,而不是基于容器的实现方式.例如,您选择的容器可能基于快速查找的需要.对于您所关心的一切,容器可以实现为单向列表 - 只要搜索速度非常快,您就会感到高兴.那是因为你无论如何都没有触及内部,你正在使用迭代器或成员函数进行访问.您的代码不受容器实现方式的限制,而是绑定到它的速度,或者它是否具有固定和定义的顺序,或者它是否在空间上有效等等.

  • 我不认为他在谈论容器实现,他在谈论一个真正的树容器本身. (10认同)
  • @JordanMelo:关于所有观点的废话.这是一个包含对象的东西.将它设计为具有begin()和end()以及双向迭代器以进行迭代是非常简单的.每个容器都有不同的特征.如果可以另外具有树特征将是有用的.应该很容易. (6认同)
  • @MooingDuck我认为wilhelmtell意味着C++标准库没有根据它们的底层数据结构定义容器; 它只通过它们的接口和渐近性能等可观察的特征来定义容器.当你想到它时,一棵树根本就不是一个容器(就像我们所知道的那样).它们甚至没有一个直接的`end()`和`begin()`,您可以使用它来遍历所有元素等. (3认同)

nob*_*bar 47

"我想将对象的层次结构存储为树"

C++ 11已经过去了,他们仍然没有看到需要提供一个std::tree,虽然这个想法确实出现了(见这里).也许他们没有添加这个的原因是,在现有容器之上构建自己的容易起来非常容易.例如...

template< typename T >
struct tree_node
   {
   T t;
   std::vector<tree_node> children;
   };
Run Code Online (Sandbox Code Playgroud)

一个简单的遍历将使用递归...

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

如果您想维护层次结构希望它与STL算法一起使用,那么事情可能会变得复杂.您可以构建自己的迭代器并实现一些兼容性,但是许多算法对层次结构没有任何意义(例如,任何改变范围顺序的东西).即使在层次结构中定义范围也可能是一项混乱的业务.

  • 事实证明,他们是懒惰的,实际上是你的第一个例子Undefined Behavior. (3认同)
  • 如果项目可以允许对tree_node的子节点进行排序,那么使用std :: set <>代替std :: vector <>然后将一个运算符<()添加到tree_node对象将大大改善'搜索''T'类对象的表现. (2认同)
  • @Mehrdad:我最后决定要求你的评论背后的细节[这里](http://stackoverflow.com/questions/32487575/class-or-struct-self-reference-by-template). (2认同)

sys*_*ult 42

如果您正在寻找RB树实现,那么stl_tree.h也可能适合您.

  • 奇怪的是,这是实际回答原始问题的唯一回应. (13认同)
  • 考虑到他想要一个"Heiarchy",似乎可以安全地假设任何具有"平衡"的东西都是错误的答案. (11认同)
  • "这是一个内部头文件,包含在其他库头文件中.您不应该尝试直接使用它." (11认同)
  • @Dan:复制它并不构成直接使用它. (3认同)

gex*_*ide 16

问题是没有一种放之四海而皆准的解决方案。而且,对于一棵树来说,甚至没有一个通用的接口。也就是说,甚至不清楚这样的树数据结构应该提供哪些方法,甚至不清楚树是什么。

这就解释了为什么没有 STL 支持:STL 是大多数人需要的数据结构,基本上每个人都同意什么是合理的接口和有效的实现。对于树木来说,这样的事情根本不存在。

血淋淋的细节

如果想进一步了解问题所在,请继续阅读。否则,上面的段落应该足以回答您的问题。

我说连通用的接口都没有。您可能不同意,因为您心中只有一个应用程序,但如果您进一步思考,您会发现对树有无数种可能的操作。您可以拥有一个可以有效实现其中大多数操作的数据结构,但因此总体上更加复杂,并且会产生这种复杂性的开销,或者您可以拥有更简单的数据结构,仅允许基本操作,但这些操作要尽可能快。

如果您想了解完整的故事,请查看我关于该主题的论文。在那里,您将找到可能的接口、不同实现的渐近复杂性、问题的一般描述以及更多可能实现的相关工作。

什么是树?

它已经从你认为是一棵树开始:

  • 有根或无根:大多数程序员想要有根,大多数数学家想要无根。(如果您想知道什么是无根树:A - B - C 是一棵树,其中 A、B 或 C 可以是根。有根树定义了哪一个是根。无根树则不然)
  • 单根/连接或多根/断开(树或森林)
  • 兄弟姐妹顺序相关吗?如果不是,那么树结构可以在更新时内部重新排序子级吗?如果是这样,则不再定义兄弟之间的迭代顺序。但对于大多数树来说,兄弟顺序实际上没有意义,并且允许数据结构在更新时对子树重新排序对于某些更新非常有益。
  • 真的只是一棵树,或者也允许 DAG 边(听起来很奇怪,但很多最初想要一棵树的人最终想要一个 DAG)
  • 有标签还是无标签?您是否需要为每个节点存储任何数据,或者只是您感兴趣的树结构(后者可以非常简洁地存储)

查询操作

在弄清楚我们定义的树之后,我们应该定义查询操作:基本操作可能是“导航到子级,导航到父级”,但是还有更多可能的操作,例如:

  • 导航到下一个/上一个同级:即使大多数人都会认为这是一个非常基本的操作,但如果您只有父指针或子数组,这实际上几乎是不可能的。因此,这已经向您表明,根据您需要的操作,您可能需要完全不同的实现。
  • 按前/后顺序导航
  • 子树大小:当前节点的(传递)后代的数量(可能是 O(1) 或 O(log n),即,不要只是将它们全部枚举出来进行计数)
  • 当前节点中树的高度。即从该节点到任意离开节点的最长路径。再次强调,时间复杂度小于 O(n)。
  • 给定两个节点,找到该节点的最不共同祖先(内存消耗为 O(1))
  • 在前序/后序遍历中,节点 A 和节点 B 之间有多少个节点?(小于 O(n) 运行时间)

我强调,这里有趣的是这些方法是否可以比 O(n) 执行得更好,因为枚举整个树始终是一种选择。根据您的应用程序,某些操作比 O(n) 更快可能绝对至关重要,或者您可能根本不在乎。同样,根据您的需求,您将需要截然不同的数据结构。

更新操作

到目前为止,我只讨论了查询操作。但现在要更新了。同样,更新树的方式有很多种。根据您的需要,您需要或多或少复杂的数据结构:

  • 叶子更新(简单):删除或添加叶子节点
  • 内部节点更新(更难):移动或删除移动内部节点,使其子节点成为其父节点的子节点
  • 子树更新(较难):移动或删除以节点为根的子树

给您一些直觉:如果您存储一个子数组并且您的同级顺序很重要,那么即使删除叶子也可能是 O(n),因为它后面的所有同级都必须在其父级的子数组中移动。如果您只有一个父指针,则叶子删除的时间复杂度仅为 O(1)。如果您不关心同级顺序,则子数组的时间复杂度也是 O(1),因为您可以简单地将间隙替换为数组中的最后一个同级。这只是一个示例,不同的数据结构将为您提供完全不同的更新功能。

在父指针的情况下,移动整个子树同样是微不足道的 O(1),但如果您有一个存储所有节点的数据结构(例如,按预定顺序),则可以是 O(n)。

然后,还有一些正交的考虑因素,例如如果执行更新,哪些迭代器保持有效。某些数据结构需要使整个树中的所有迭代器无效,即使插入新叶子也是如此。其他的仅使树中被更改的部分的迭代器无效。其他人保持所有迭代器(删除节点的迭代器除外)有效。

空间考虑

树结构可以非常简洁。如果您需要节省空间(例如 DFUDS 或 LOUDS,请参阅此说明以了解要点),每个节点大约两位就足够了。但当然,天真地,即使父指针也已经是 64 位了。一旦您选择了易于导航的结构,您可能宁愿每个节点需要 20 个字节。

通过非常复杂的技术,我们还可以构建一种数据结构,每个条目只需要一些位,可以有效地更新,并且仍然可以渐近快速地启用所有查询操作,但这是一种高度复杂的结构的野兽。我曾经开设过一门实践课程,让研究生实施这篇论文。他们中的一些人能够在 6 周内实施(!),另一些则失败了。虽然该结构具有很大的渐近性,但其复杂性使其对于非常简单的操作有相当大的开销。

再次强调,没有一刀切的方法。

结论

我花了 5 年的时间寻找表示树的最佳数据结构,尽管我想出了一些并且有相当多的相关工作,但我的结论是不存在。根据用例,简单的父指针将优于高度复杂的数据结构。即使为树定义接口也很困难。我尝试在论文中定义一个接口,但我必须承认,在许多用例中,我定义的接口太窄或太大。所以我怀疑这是否会最终出现在 STL 中,因为调节旋钮太多了。


J.J*_*.J. 12

std :: map基于一棵红黑树.您还可以使用其他容器来帮助您实现自己的树类型.

  • 它通常使用红黑树(不需要这样做). (13认同)

Ecl*_*pse 8

在某种程度上,std :: map是一棵树(它需要具有与平衡二叉树相同的性能特征),但它不会暴露其他树功能.不包括真实树数据结构的可能原因可能只是不包括stl中的所有内容.可以将stl看作是用于实现自己的算法和数据结构的框架.

一般来说,如果你想要一个基本的库功能,那就不是在stl中,修复就是看看BOOST.

否则,有一个一堆 那里,这取决于你的树的需求.


Emi*_*lia 6

所有STL容器外部表示为具有一个迭代机制的"序列".树木不遵循这个习语.

  • 树数据结构可以通过迭代器提供预订,顺序或后序遍历.实际上这就是std :: map的作用. (6认同)
  • 是的,不是......这取决于你对"树"的意思.`std :: map`在内部实现为btree,但在外部它显示为PAIRS的排序SEQUENCE.无论你有什么元素,你都可以普遍地问谁以前和谁在追求.包含元素的一般树结构中的每一个都包含其他元素,不会强加任何排序或方向.你可以定义以多种方式遍历树结构的迭代器(sallow | deep first | last ...)但是一旦你做到了,`std :: tree`容器必须从`begin`函数返回其中一个.并没有明显的理由让一个或另一个回归. (3认同)
  • std :: map通常由平衡二叉搜索树表示,而不是B树.您所做的相同参数可以应用于std :: unordered_set,它没有自然顺序,但提供了开始和结束迭代器.开始和结束的要求只是它以某种确定性顺序迭代所有元素,而不是必须有一个自然元素.preorder是树的完全有效的迭代顺序. (3认同)
  • 你的答案的含义是没有stl n-tree数据结构,因为它没有"序列"接口.这完全是错误的. (3认同)
  • @EmiloGaravaglia:由`std :: unordered_set`证明,它没有迭代其成员的"独特方式"(实际上迭代顺序是伪随机和实现定义),但仍然是一个stl容器 - 这反驳了你的观点.即使订单未定义,迭代容器中的每个元素仍然是一项有用的操作. (2认同)
  • 容器没有理由需要有一种行走方式,可以通过不同类型的迭代器对提供不同的迭代顺序(例如参见`rbegin`/`rend`)。std:tree 可以分别具有类似 `breadth_begin`/`breadth_end` 和 `depth_begin`/`depth_end` 的上下和左右顺序。您链接的答案是正确的,潜在结构的巨大差异导致它被排除在标准库之外,但这与“序列”或“一次迭代机制”无关。 (2认同)

Pau*_*han 5

因为 STL 不是“一切”库。从本质上讲,它包含构建事物所需的最少结构。

  • 二叉树是一个非常基本的功能,事实上,比其他容器更基本,因为像 std::map、std::multimap 和 stl::set 这样的类型。由于这些类型是基于它们的,因此您会期望暴露底层类型。 (13认同)
  • “构建事物的最小结构”是一个非常主观的陈述。你可以用原始的 C++ 概念来构建东西,所以我认为真正的最低限度是根本没有 STL。 (7认同)
  • 我不认为 OP 要求的是 _binary_ 树,他要求的是一棵树来存储层次结构。 (2认同)

小智 5

这个看起来很有希望,似乎正是您正在寻找的:http : //tree.phi-sci.com/


bob*_*obo 5

IMO,一个遗漏。但我认为有充分的理由不在 STL 中包含树结构。维护一棵树有很多逻辑,最好将其作为成员函数写入基础TreeNode对象中。当TreeNode它包含在 STL 标头中时,它会变得更加混乱。

例如:

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); } 
} ;
Run Code Online (Sandbox Code Playgroud)

  • 那里有很多拥有原始指针,其中许多根本不需要成为指针。 (8认同)

小智 5

我认为没有 STL 树有几个原因。树主要是递归数据结构的一种形式,它与容器(列表、向量、集合)一样,具有非常不同的精细结构,这使得正确的选择变得棘手。它们也很容易使用 STL 以基本形式构建。

一个有限根树可以被认为是一个容器,它有一个值或有效载荷,比如一个 A 类的实例和一个可能为空的有根(子)树集合;具有空子树集合的树被认为是叶子。

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
{};
Run Code Online (Sandbox Code Playgroud)

必须考虑一下迭代器设计等,以及允许定义哪些乘积和联积操作并在树之间有效 - 并且原始 STL 必须写得很好 - 以便空集、向量或列表容器是在默认情况下,真的没有任何有效负载。

树在许多数学结构中发挥着重要作用(参见 Butcher、Grossman 和 Larsen 的经典论文;还有 Connes 和 Kriemer 的论文,以了解它们可以连接的示例以及如何使用它们进行枚举)。认为它们的作用只是促进某些其他操作是不正确的。相反,它们促进了这些任务,因为它们作为数据结构的基本作用。

然而,除了树之外,还有“co-trees”;上面所有的树都具有这样的特性:如果您删除根,您将删除所有内容。

考虑树上的迭代器,可能它们会被实现为一个简单的迭代器堆栈,一个节点,一个节点,它的父节点,......直到根。

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

但是,您可以拥有任意数量的;它们共同形成一个“树”,但是所有箭头都朝着根的方向流动,这个联合树可以通过迭代器向平凡迭代器和根迭代;然而,它不能被跨或向下导航(其他迭代器不知道),也不能删除迭代器的集合,除非跟踪所有实例。

树非常有用,它们有很多结构,这使得获得绝对正确的方法成为一个严峻的挑战。在我看来,这就是它们没有在 STL 中实现的原因。此外,在过去,我看到人们变得虔诚并发现一种包含其自身类型实例的容器的想法具有挑战性 - 但他们必须面对它 - 这就是树类型所代表的 - 它是一个包含可能是空的(较小的)树集合。当前的语言允许它毫无挑战地提供默认构造函数 for container<B>not 在堆(或其他任何地方)上为 anB等分配空间。

如果这确实以良好的形式进入标准,我会很高兴。