Как перебирать древовидную структуру с помощью генератора?

Я пытаюсь понять, как реализовать функцию в узле дерева, которая возвращает все его дочерние листья (прямые или косвенные). Однако я не хочу передавать контейнер, в котором листовые узлы будут помещаться рекурсивно (дерево может быть огромным), вместо этого я хотел бы использовать генератор для итерации по дереву. Я пробовал несколько подходов, но пока ни один из них не работал. Это самое близкое к возможному решению, которое я подошел:

    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.

Любая помощь будет очень высоко ценится. Заранее спасибо.

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

Да - пометил. Тег «Программирование» избыточен на сайте программирования. знак равно

Erik Forbes 31.12.2008 01:09
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
1
1 522
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Вот как следует реализовать 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 может иметь другие ответвления в качестве дочерних, и поэтому я использовал рекурсию.

Trap 30.12.2008 23:42

Теперь это должно сработать, теперь он будет правильно использовать рекурсию.

Lasse V. Karlsen 30.12.2008 23:44

Итак, в основном решение состоит в том, чтобы создавать новый генератор каждый раз, когда вы углубляетесь в рекурсию?

Trap 31.12.2008 00:21

Итак, это будет итерация O (n log n) по дереву?

Jules 31.12.2008 01:11

См. Ответ Эрика для получения дополнительной информации о производительности. Как всегда, профилируйте код и, если это узкое место, проведите рефакторинг.

Lasse V. Karlsen 31.12.2008 01:17

O (n ^ 2) на самом деле. См. Ссылку внизу моего ответа, чтобы узнать, почему.

Erik Forbes 31.12.2008 01:18

Прочитал ссылку. Это O (n log n). Это потому, что глубина рекурсии для обхода дерева равна log n, а не n.

recursive 31.12.2008 01:31

Это зависит от того, что такое n и насколько сбалансировано ваше дерево :) Если ваше дерево глубокое, но не широкое, оно будет ближе к O (n ^ 2), где n - количество узлов в дереве.

Daniel Plaisted 31.12.2008 03:10

O (n ^ 2) худший случай, я должен был сказать. Извинения.

Erik Forbes 31.12.2008 07:31

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/…

Daniel Plaisted 31.12.2008 00:58

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

Erik Forbes 31.12.2008 01:06

Другие ответы верны, но шаблон возврата 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)? Я этого не вижу.

Erik Forbes 31.12.2008 01:22

Это O (n log n). В предоставленной вами ссылке глубина рекурсии была n. В дереве в среднем это log n.

recursive 31.12.2008 01:32

Я обновил его, чтобы указать, что время выполнения рекурсивного метода равно O (количество узлов * средняя глубина узла). У этого должно быть время выполнения всего O (количество узлов), что такое же, как у Эрика. Теоретически у этого меньше памяти.

Daniel Plaisted 31.12.2008 03:08

Другие вопросы по теме