Я пытаюсь понять, как реализовать функцию в узле дерева, которая возвращает все его дочерние листья (прямые или косвенные). Однако я не хочу передавать контейнер, в котором листовые узлы будут помещаться рекурсивно (дерево может быть огромным), вместо этого я хотел бы использовать генератор для итерации по дереву. Я пробовал несколько подходов, но пока ни один из них не работал. Это самое близкое к возможному решению, которое я подошел:
public interface ITreeNode
{
IEnumerable<ITreeNode> EnumerateLeaves();
}
class Leaf : ITreeNode
{
public IEnumerable<ITreeNode> EnumerateLeaves()
{
throw new NotImplementedException();
}
}
class Branch : ITreeNode
{
private List<ITreeNode> m_treeNodes = new List<ITreeNode>();
public IEnumerable<ITreeNode> EnumerateLeaves()
{
foreach( var node in m_treeNodes )
{
if ( node is Leaf )
yield return node;
else
node.EnumerateLeaves();
}
}
}
Но это тоже не работает. Что я делаю неправильно? Похоже, что рекурсивный вызов .EnumerateLeaves не будет работать, если в той же функции есть оператор yield.
Любая помощь будет очень высоко ценится. Заранее спасибо.
Обновлено: я забыл упомянуть, что ветка может иметь листья или ветки в качестве дочерних элементов, отсюда и рекурсия.





Вот как следует реализовать Branch.EnumerateLeaves:
public IEnumerable<ITreeNode> EnumerateLeaves()
{
foreach( var node in m_treeNodes )
{
if ( node is Leaf )
yield return node;
else
{
foreach (ITreeNode childNode in node.EnumerateLeaves())
yield return childNode;
}
}
}
Это тоже не сработает, так как Branch может иметь другие ответвления в качестве дочерних, и поэтому я использовал рекурсию.
Теперь это должно сработать, теперь он будет правильно использовать рекурсию.
Итак, в основном решение состоит в том, чтобы создавать новый генератор каждый раз, когда вы углубляетесь в рекурсию?
Итак, это будет итерация O (n log n) по дереву?
См. Ответ Эрика для получения дополнительной информации о производительности. Как всегда, профилируйте код и, если это узкое место, проведите рефакторинг.
O (n ^ 2) на самом деле. См. Ссылку внизу моего ответа, чтобы узнать, почему.
Прочитал ссылку. Это O (n log n). Это потому, что глубина рекурсии для обхода дерева равна log n, а не n.
Это зависит от того, что такое n и насколько сбалансировано ваше дерево :) Если ваше дерево глубокое, но не широкое, оно будет ближе к O (n ^ 2), где n - количество узлов в дереве.
O (n ^ 2) худший случай, я должен был сказать. Извинения.
lassevk прав - одна потенциальная проблема с этим методом, однако, заключается в том, что рекурсивный вызов enumerables может дать производительность O (n ^ 2). Если это проблема, вы должны вместо этого исключить рекурсию и использовать внутренний стек.
public IEnumerable<ITreeNode> EnumerateLeaves()
{
Stack<ITreeNode> workStack = new Stack<ITreeNode>(m_treeNodes);
while(workStack.Count > 0) {
var current = workStack.Pop();
if (current is Leaf)
yield return current;
else {
for(n = 0; n < current.m_treeNodes.Count; n++) {
workStack.Push(current.m_treeNodes[n]);
}
}
}
}
Это должно выполнять ту же функцию без рекурсии.
Обновлено: Дэниел Плейстед опубликовано в комментарии о более серьезной проблеме с рекурсивным вызовом перечислителей, обобщенной в сообщение в блогах MSDN об итераторах. Спасибо, Даниэль. знак равно
Другое редактирование: всегда помните, что простота кода может быть важнее производительности, особенно если вы ожидаете, что другие будут поддерживать ваш код. Если вы не ожидаете, что ваше дерево станет очень большим, используйте метод рекурсии lassevk, описанный в его ответе.
Сборщик мусора - не самая большая проблема с рекурсией, это тот факт, что на каждой итерации он должен спускаться по «дереву» счетчиков сверху. Это может значительно увеличить время выполнения. См. blogs.msdn.com/wesdyer/archive/2007/03/23/…
Это очень верно - хотя я не мог придумать, как это сформулировать. Спасибо за ссылку - очень помогло. знак равно
Другие ответы верны, но шаблон возврата yield внутри цикла foreach с рекурсивным вызовом приведет к тому, что время выполнения итерации по дереву будет примерно таким, как O (количество узлов * средняя глубина узла). См. это сообщение в блоге для более подробной информации о проблеме.
Вот попытка генератора, который эффективен как во время выполнения, так и в использовании памяти:
class Node
{
List<Node> _children;
public bool IsLeaf { get { return _children.Count == 0; } }
public IEnumerable<Node> Children { get { return _children; } }
public IEnumerable<Node> EnumerateLeaves()
{
if (IsLeaf)
{
yield return this;
yield break;
}
var path = new Stack<IEnumerator<Node>>();
path.Push(Children.GetEnumerator());
while(!path.Empty)
{
var cur = path.Pop();
if (cur.MoveNext())
{
path.Push(cur);
if (cur.IsLeaf)
{
yield return cur;
}
else
{
path.Push(cur.Children.GetEnumerator());
}
}
}
}
}
Вы можете объяснить, чем это лучше, чем O (n ^ 2)? Я этого не вижу.
Это O (n log n). В предоставленной вами ссылке глубина рекурсии была n. В дереве в среднем это log n.
Я обновил его, чтобы указать, что время выполнения рекурсивного метода равно O (количество узлов * средняя глубина узла). У этого должно быть время выполнения всего O (количество узлов), что такое же, как у Эрика. Теоретически у этого меньше памяти.
Да - пометил. Тег «Программирование» избыточен на сайте программирования. знак равно