Почему C ++ STL не предоставляет никаких «древовидных» контейнеров?

373

Почему C ++ STL не предоставляет никаких «древовидных» контейнеров, и что лучше использовать вместо этого?

Я хочу хранить иерархию объектов в виде дерева, а не использовать дерево для повышения производительности ...

Родди
источник
7
Мне нужно дерево для хранения представления иерархии.
Родди
20
Я с парнем, который проголосовал за «правильные» ответы, которые, кажется,; «Деревья бесполезны». Существуют важные, если неясные виды использования деревьев.
Джо, приносящий душ
Я думаю, что причина тривиальна - никто еще не реализовал ее в стандартной библиотеке. Как будто стандартной библиотеки не было std::unordered_mapи std::unordered_setдо недавнего времени. А до этого в стандартной библиотеке не было контейнеров STL.
док
1
Мои мысли (хотя я никогда не читал соответствующий стандарт, поэтому это комментарий, а не ответ) заключаются в том, что STL не заботится о конкретных структурах данных, он заботится о спецификациях, касающихся сложности и поддерживаемых операций. Таким образом, используемая базовая структура может варьироваться между реализациями и / или целевыми архитектурами, если она удовлетворяет спецификациям. Я почти уверен, std::mapи std::setбуду использовать дерево в каждой реализации, но им не нужно, если какая-то не древовидная структура также соответствует спецификациям.
Марк К Коуэн

Ответы:

182

Есть две причины, по которым вы можете использовать дерево:

Вы хотите отразить проблему, используя древовидную структуру:
для этого у нас есть библиотека графов повышения

Или вам нужен контейнер с древовидными характеристиками доступа. Для этого мы имеем

В основном характеристики этих двух контейнеров таковы, что они практически должны быть реализованы с использованием деревьев (хотя на самом деле это не является обязательным требованием).

Смотрите также этот вопрос: Реализация дерева C

Мартин Йорк
источник
64
Существует множество причин использовать дерево, даже если они являются наиболее распространенными. Самый обычный! Равный всем.
Джо, приносящий душ
3
Третья основная причина, по которой нужно дерево, - это всегда отсортированный список с быстрой вставкой / удалением, но для этого есть std: multiset.
VoidStar
1
@Durga: Не уверен, насколько важна глубина, когда вы используете карту в качестве отсортированного контейнера. Карта гарантирует запись (n) вставки / удаления / поиска (и содержит элементы в отсортированном порядке). Это все карта используется и реализуется (как правило) в виде красного / черного дерева. Красно-черное дерево гарантирует, что дерево сбалансировано. Таким образом, глубина дерева напрямую связана с количеством элементов в дереве.
Мартин Йорк,
14
Я не согласен с этим ответом, как в 2008 году, так и сейчас. Стандартная библиотека не имеет «надстройки», и наличие чего-то в надстройке не должно (и не было) причиной не принимать это в стандарт. Кроме того, BGL является общим и достаточно вовлеченным, чтобы заслужить независимые от него специализированные классы дерева. Кроме того, тот факт, что для std :: map и std :: set требуется дерево, - это IMO, еще один аргумент для наличия и stl::red_black_treeт. Д. Наконец, std::mapи std::setдеревья сбалансированы, а std::treeможет и не быть.
einpoklum
1
@einpoklum: «наличие чего-либо в надстройке не должно быть причиной, чтобы не принимать это в стандарт» - учитывая, что одна из целей наддува - выступать в качестве испытательного полигона для полезных библиотек перед включением в стандарт, я могу только сказать "абсолютно!"
Мартин Боннер поддерживает Монику
94

Вероятно, по той же причине, что в надстройке нет контейнера с деревом. Есть много способов реализовать такой контейнер, и нет хорошего способа удовлетворить всех, кто будет его использовать.

Некоторые вопросы для рассмотрения:

  • Является ли число дочерних элементов для узла фиксированным или переменным?
  • Сколько накладных расходов на узел? - вам нужны родительские указатели, указатели братьев и сестер и т. д.
  • Какие алгоритмы предоставить? - разные итераторы, алгоритмы поиска и т. д.

В конце концов, проблема в том, что контейнер дерева, который был бы достаточно полезным для всех, был бы слишком тяжелым, чтобы удовлетворить большинство людей, использующих его. Если вы ищете что-то мощное, Boost Graph Library - это, по сути, расширенный набор возможностей библиотеки дерева.

Вот некоторые другие реализации общего дерева:

Грег Роджерс
источник
5
«... нет хорошего способа удовлетворить всех ...» За исключением того, что поскольку stl :: map, stl :: multimap и stl :: set основаны на rb_tree stl, он должен удовлетворять так же, как и эти базовые типы ,
Catskul
44
Учитывая, что нет способа извлечь дочерние элементы узла std::map, я бы не назвал эти контейнеры дерева. Это ассоциативные контейнеры, которые обычно реализуются в виде деревьев. Большая разница.
Mooing Duck
2
Я согласен с Mooing Duck, как бы вы реализовали поиск в ширину на std :: map? Это будет ужасно дорого
Марко А.
1
Я начал использовать tree.hh из Kasper Peeters, однако после проверки лицензии на GPLv3 или любую другую версию GPL это может привести к заражению нашего коммерческого программного обеспечения. Я бы порекомендовал посмотреть на treetree, предоставленный в комментарии @hplbsh, если вам нужна структура для коммерческих целей.
Jake88
3
Специфичные требования к деревьям - это аргумент в пользу того, чтобы иметь разные типы деревьев, а не иметь их вообще.
Андре
50

Философия STL заключается в том, что вы выбираете контейнер на основе гарантий, а не на основе того, как контейнер реализован. Например, ваш выбор контейнера может быть основан на необходимости быстрого поиска. Как ни крути, контейнер может быть реализован как однонаправленный список - если поиск очень быстрый, вы будете счастливы. Это потому, что вы все равно не касаетесь внутренних органов, вы используете итераторы или функции-члены для доступа. Ваш код связан не с тем, как реализован контейнер, а с тем, насколько он быстр, или имеет ли он фиксированный и определенный порядок, или он эффективен в пространстве, и так далее.

wilhelmtell
источник
12
Я не думаю, что он говорит о реализации контейнера, он говорит о самом фактическом контейнере дерева.
Mooing Duck
3
@MooingDuck Я думаю, что wilhelmtell означает, что стандартная библиотека C ++ не определяет контейнеры на основе их базовой структуры данных; он определяет контейнеры только по их интерфейсу и наблюдаемым характеристикам, таким как асимптотические характеристики. Когда вы думаете об этом, дерево на самом деле вовсе не является контейнером (как мы их знаем). Они даже не имеют аа прямой end()и begin()с помощью которого вы можете перебрать все элементы и т.д.
Jordan Melo
7
@JordanMelo: ерунда по всем пунктам. Это вещь, которая содержит объекты. Очень просто спроектировать так, чтобы иметь итераторы begin () и end () и двунаправленные итерации. Каждый контейнер имеет разные характеристики. Было бы полезно, если бы можно было дополнительно иметь характеристики дерева. Должно быть довольно легко.
Mooing Duck
Таким образом, нужно иметь контейнер, который обеспечивает быстрый поиск для дочерних и родительских узлов и разумные требования к памяти.
Док
@JordanMelo: С этой точки зрения также адаптеры, такие как очереди, стеки или приоритетные очереди, не будут принадлежать STL (у них также нет begin()и end()). И помните, что приоритетная очередь, как правило, представляет собой кучу, которая, по крайней мере, теоретически является деревом (даже при реальных реализациях). Таким образом, даже если вы реализовали дерево в качестве адаптера с использованием какой-либо другой базовой структуры данных, его можно будет включить в STL.
Андре
48

«Я хочу хранить иерархию объектов в виде дерева»

C ++ 11 приходил и уходил, и они все еще не видели необходимости предоставлять std::tree, хотя идея все же появилась (см. Здесь ). Возможно, причина, по которой они не добавили это, состоит в том, что легко построить свой собственный поверх существующих контейнеров. Например...

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

Простой обход будет использовать рекурсию ...

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

Если вы хотите поддерживать иерархию и хотите, чтобы она работала с алгоритмами STL , то все может быть сложно. Вы можете создать свои собственные итераторы и достичь некоторой совместимости, однако многие алгоритмы просто не имеют никакого смысла для иерархии (например, всего, что изменяет порядок диапазона). Даже определение диапазона в иерархии может быть грязным делом.

nobar
источник
2
Если проект может разрешить сортировку потомков tree_node, то использование std :: set <> вместо std :: vector <> и последующее добавление оператора <() к объекту tree_node значительно улучшат «поиск» производительности T-подобного объекта.
J Jorgenson
4
Оказывается, они были ленивы и фактически сделали ваш первый пример Undefined Behavior.
user541686
2
@Mehrdad: Я наконец-то решил спросить детали вашего комментария здесь .
Нобар
many of the algorithms simply don't make any sense for a hierarchy, Вопрос интерпретации. Представьте себе структуру пользователей stackoverflow, и каждый год вы хотите, чтобы те, у кого больше очков репутации, возглавляли тех, у кого очки репутации ниже. Таким образом, обеспечивая BFS итератор и соответствующее сравнение, каждый год вы просто запускаете std::sort(tree.begin(), tree.end()).
Док
К тому же, вы можете легко построить ассоциативный дерево (для моделирования неструктурированных записей ключ-значение, как JSON, например), заменив vectorс mapв приведенном выше примере. Для полной поддержки JSON-подобной структуры вы можете использовать variantдля определения узлов.
nobar
43

Если вы ищете реализацию RB-дерева, то stl_tree.h также может подойти вам.

systemsfault
источник
14
Странно, что это единственный ответ, который на самом деле отвечает на оригинальный вопрос.
Catskul
12
Учитывая, что он хочет «Иерархию», кажется безопасным предположить, что что-то с «балансированием» является неправильным ответом.
Mooing Duck
11
«Это внутренний файл заголовка, включенный другими заголовками библиотеки. Вы не должны пытаться использовать его напрямую».
Дан
3
@Dan: копирование не означает использование его напрямую.
einpoklum
12

std :: map основана на красном черном дереве . Вы также можете использовать другие контейнеры, чтобы помочь вам реализовать свои собственные типы деревьев.

JJ
источник
13
Обычно используются красно-черные деревья (это не обязательно).
Мартин Йорк,
1
GCC использует дерево для реализации карты. Кто-нибудь хочет взглянуть на каталог VC include, чтобы узнать, что использует Microsoft?
JJ
// Класс красно-черного дерева, предназначенный для использования при реализации STL // ассоциативных контейнеров (set, multiset, map и multimap). Получил это из моего файла stl_tree.h.
JJ
@JJ По крайней мере в Studio 2010 он использует внутренний ordered red-black tree of {key, mapped} values, unique keysкласс, определенный в <xtree>. У вас нет доступа к более современной версии прямо сейчас.
Джастин Тайм - Восстановить Монику
8

В некотором смысле, std :: map - это дерево (оно должно иметь те же характеристики производительности, что и сбалансированное двоичное дерево), но оно не предоставляет другие функциональные возможности дерева. Вероятная причина, по которой не была включена структура данных реального дерева, была, вероятно, просто вопросом не включения всего в stl. Stl можно рассматривать как основу для реализации ваших собственных алгоритмов и структур данных.

В общем, если вам нужна базовая функциональность библиотеки, которой нет в stl, то исправление - посмотреть на BOOST .

В противном случае, есть куча из библиотек из там , в зависимости от потребностей вашего дерева.

Затмение
источник
6

Все контейнеры STL внешне представлены как «последовательности» с одним итерационным механизмом. Деревья не следуют этой идиоме.

Эмилио Гаравалья
источник
7
Древовидная структура данных может обеспечить предварительный, внутренний или обратный порядок прохождения через итераторы. Фактически это то, что делает std :: map.
Эндрю Томазос
3
Да и нет ... это зависит от того, что вы подразумеваете под "деревом". std::mapвнутренне реализовано как btree, но внешне оно выглядит как отсортированная ПОСЛЕДОВАТЕЛЬНОСТЬ ПАР. Учитывая любой элемент, вы всегда можете спросить, кто до, а кто после. Общие древовидные структуры, содержащие элементы, каждый из которых содержит другие, не накладывают никакой сортировки или направления. Вы можете определить итераторы, которые обходят древовидную структуру разными способами (sallow | deep first | last ...), но как только вы это сделаете, std::treeконтейнер должен вернуть один из них из beginфункции. И нет явных причин возвращать то или иное.
Эмилио Гаравалья
4
Std :: map обычно представляется сбалансированным бинарным деревом поиска, а не B-деревом. Тот же аргумент, который вы указали, может применяться к std :: unordered_set, он не имеет естественного порядка, но представляет итераторы начала и конца. Требование начала и конца состоит в том, что он выполняет итерацию всех элементов в некотором детерминированном порядке, а не в том, что он должен быть естественным. Предзаказ - это совершенно правильный порядок итераций для дерева.
Эндрю Томазос
4
Смысл вашего ответа в том, что нет структуры данных stl n-tree, потому что она не имеет интерфейса «sequence». Это просто неверно.
Эндрю Томазос
3
@EmiloGaravaglia: Как свидетельствует std::unordered_set, у которого нет «уникального способа» итерации своих членов (фактически, порядок итерации псевдослучайен и определяется реализацией), но все еще является контейнером stl - это опровергает вашу точку зрения. Повторение каждого элемента в контейнере все еще является полезной операцией, даже если порядок не определен.
Эндрю Томазос
4

Потому что STL не является библиотекой "всего". Он содержит, по сути, минимальные структуры, необходимые для создания вещей.

Пол Натан
источник
13
Двоичные деревья - это чрезвычайно базовая функциональность, и на самом деле они более простые, чем другие контейнеры, поскольку такие типы, как std :: map, std :: multimap и stl :: set. Так как эти типы основаны на них, вы ожидаете, что будет раскрыт базовый тип.
Catskul
2
Я не думаю, что ОП просит двоичное дерево, он просит дерево для хранения иерархии.
Утка
Мало того, что добавление «контейнера» дерева в STL означало бы добавление множества новых концепций, например, навигатора дерева (обобщающего Iterator).
alfC
5
«Минимальные структуры для построения вещей» - очень субъективное утверждение. Вы можете создавать вещи с использованием необработанных концепций C ++, так что я предполагаю, что истинным минимумом будет отсутствие STL.
Док
3

ИМО, упущение. Но я думаю, что есть веская причина не включать древовидную структуру в 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); } 
} ;
bobobobo
источник
7
У вас есть много сырых указателей, многие из которых вообще не нуждаются в указателях.
Mooing Duck
Предложите отозвать этот ответ. Класс TreeNode является частью реализации дерева.
einpoklum
3

Я думаю, что есть несколько причин, почему нет деревьев 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
{};

Нужно немного подумать о дизайне итератора и т. Д. И о том, какие операции с продуктом и сопутствующим продуктом можно определять и быть эффективными между деревьями - и исходный STL должен быть хорошо написан - так, чтобы пустой контейнер набора, вектора или списка был действительно пустой из любой полезной нагрузки в случае по умолчанию.

Деревья играют существенную роль во многих математических структурах (см. Классические статьи Бучера, Гроссмана и Ларсена; также работы Конна и Кримера для примеров их объединения и того, как они используются для перечисления). Неправильно думать, что их роль заключается просто в облегчении некоторых других операций. Скорее они облегчают эти задачи из-за их фундаментальной роли как структуры данных.

Тем не менее, в дополнение к деревьям есть также «деревья»; деревья, прежде всего, обладают тем свойством, что при удалении корня вы удаляете все.

Рассмотрим итераторы на дереве, возможно, они будут реализованы как простой стек итераторов для узла и его родителя ... вплоть до корня.

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

Тем не менее, вы можете иметь столько, сколько хотите; вместе они образуют «дерево», но там, где все стрелки текут в направлении к корню, это совместное дерево может быть перебрано с помощью итераторов к тривиальным итераторам и корню; однако он не может перемещаться по или вниз (другие итераторы ему неизвестны), и ансамбль итераторов не может быть удален, кроме как путем отслеживания всех экземпляров.

Деревья невероятно полезны, у них много структуры, поэтому очень сложно получить окончательно правильный подход. На мой взгляд, именно поэтому они не реализованы в STL. Более того, в прошлом я видел, как люди становятся религиозными и находят идею типа контейнера, содержащего экземпляры своего собственного типа, сложной - но им приходится с этим сталкиваться - это то, что представляет собой тип дерева - это узел, содержащий возможно пустая коллекция (меньших) деревьев. Текущий язык разрешает это без проблем, предоставляя конструктор по умолчанию для container<B>не выделяет место в куче (или где-либо еще) для и Bт. Д.

Я, например, был бы рад, если бы это в хорошей форме нашло свое отражение в стандарте.

tjl
источник
0

При чтении ответов здесь часто встречаются названные причины: нельзя перебирать дерево или что дерево не принимает интерфейс, аналогичный другим контейнерам STL, и нельзя использовать алгоритмы STL с такой структурой дерева.

Имея это в виду, я попытался спроектировать свою собственную древовидную структуру данных, которая обеспечит STL-подобный интерфейс и будет максимально использоваться с существующими алгоритмами STL.

Моя идея заключалась в том, что дерево должно быть основано на существующих контейнерах STL и не должно скрывать контейнер, чтобы оно было доступно для использования с алгоритмами STL.

Другая важная особенность, которую должно обеспечить дерево, это итераторы обхода.

Вот что я смог придумать: https://github.com/igagis/utki/blob/master/src/utki/tree.hpp

И вот тесты: https://github.com/igagis/utki/blob/master/tests/tree/tests.cpp

igagis
источник
-9

Все контейнеры STL могут использоваться с итераторами. Вы не можете иметь итератор для дерева, потому что у вас нет «одного правильного» способа пройтись по дереву.

7890
источник
3
Но вы можете сказать, что BFS или DFS - правильный путь. Или поддержите их обоих. Или любой другой, который вы можете себе представить. Просто скажи пользователю, что это такое.
tomas789
2
в std :: map есть итератор дерева.
Джай
1
Дерево может определять свой собственный тип итератора, который пересекает все узлы в порядке от одного «крайнего» к другому (то есть для любого двоичного дерева с путями 0 и 1 оно может предложить итератор, который переходит от «всех 0» к «всем» 1s «и обратный итератор , который делает обратный; для дерева с глубиной 3 и исходным узлом s, например, он может перебирать узлы , как s000, s00, s001, s0, s010, s01, s011, s, s100, s10, s101, s1, s110, s11, s111(» крайний левый»на„крайний правый“), он также может использовать шаблон глубины обхода ( s, s0, s1, s00, s01, s10, s11,
Джастин Тайм - Восстановить Монику
и т. д.) или какой-либо другой шаблон, если он проходит по каждому узлу таким образом, что каждый из них передается только один раз.
Джастин Тайм - Восстановить Монику
1
@doc, очень хорошая мысль. Я думаю, что std::unordered_setбыла «сделана» последовательность, потому что мы не знаем лучшего способа перебора элементов, отличного от произвольного (внутренне заданного хэш-функцией). Я думаю, что это противоположный случай дерева: итерация по unordered_setнедоопределена, в теории нет «никакого способа» определить итерацию, кроме, возможно, «случайным образом». В случае дерева существует много «хороших» (неслучайных) способов. Но, опять же, ваша точка зрения верна.
AlfC