Как "развернуть" "рекурсивную" структуру

Не уверен, как это назвать, но скажем, у вас есть класс, который выглядит так:

class Person
{
    public string Name;
    public IEnumerable<Person> Friends;
}

Затем у вас есть человек, и вы хотите рекурсивно "развернуть" эту структуру, чтобы вы получили единый список всех людей без дубликатов.

Как бы вы это сделали? Я уже сделал что-то, что, кажется, работает, но мне любопытно посмотреть, как это сделают другие, особенно если в Linq есть что-то встроенное, что вы можете использовать умным способом для решения этой маленькой проблемы:)


Вот мое решение:

public static IEnumerable<T> SelectRecursive<T>(this IEnumerable<T> subjects, Func<T, IEnumerable<T>> selector)
{
    // Stop if subjects are null or empty
    if(subjects == null)
        yield break;

    // For each subject
    foreach(var subject in subjects)
    {
        // Yield it
        yield return subject;

        // Then yield all its decendants
        foreach (var decendant in SelectRecursive(selector(subject), selector))
            yield return decendant;
    }
}

Будет использоваться что-то вроде этого:

var people = somePerson.SelectRecursive(x => x.Friends);

7 ответов

Решение

Я не верю, что в LINQ есть что-то для этого.

Существует проблема с рекурсивным выполнением этого - в итоге вы создаете большое количество итераторов. Это может быть совершенно неэффективно, если дерево глубоко. Уэс Дайер и Эрик Липперт писали об этом в блоге.

Вы можете устранить эту неэффективность, удалив прямую рекурсию. Например:

public static IEnumerable<T> SelectRecursive<T>(this IEnumerable<T> subjects,
    Func<T, IEnumerable<T>> selector)
{
    if (subjects == null)
    {
        yield break;
    }

    Queue<T> stillToProcess = new Queue<T>(subjects);

    while (stillToProcess.Count > 0)
    {
        T item = stillToProcess.Dequeue();
        yield return item;
        foreach (T child in selector(item))
        {
            stillToProcess.Enqueue(child);
        }
    }
}

Это также изменит порядок итераций - он становится шириной, а не глубиной; переписать его так, чтобы он все еще был первым, сложно. Я также изменил это, чтобы не использовать Any() - эта пересмотренная версия не будет оценивать последовательность более одного раза, что может быть полезно в некоторых сценариях. Имейте в виду, есть одна проблема - из-за очередей потребуется больше памяти. Мы могли бы, вероятно, смягчить это, сохранив очередь итераторов вместо элементов, но я не уверен, что это было бы необоснованно... это, безусловно, было бы более сложным.

Одно замечание (также отмеченное ChrisWe, когда я просматривал сообщения в блоге:) - если у вас есть какие-либо циклы в списке друзей (то есть, если у A есть B, а у B есть A), вы будете возвращаться навсегда.

Я нашел этот вопрос, когда искал и думал о подобном решении - в моем случае создание эффективного IEnumerable<Control> для элементов управления ASP.NET UI. Рекурсивный yield У меня было быстро, но я знал, что это может привести к дополнительным затратам, поскольку чем глубже структура управления, тем дольше это может занять. Теперь я знаю, что это O(n log n).

Решение, приведенное здесь, дает некоторый ответ, но, как обсуждалось в комментариях, оно меняет порядок (который ОП не заботил). Я понял, что для сохранения порядка, заданного ФП и, как мне было нужно, ни Queue (как использовал Джон) ни Stack будет работать, так как сначала будут получены все родительские объекты, а затем любые дочерние объекты после них (или наоборот).

Чтобы решить эту проблему и сохранить порядок, я понял, что решение будет просто положить Enumerator сам на Stack, Если использовать оригинальный вопрос ОП, это будет выглядеть так:

public static IEnumerable<T> SelectRecursive<T>(this IEnumerable<T> subjects, Func<T, IEnumerable<T>> selector)
{
    if (subjects == null)
        yield break;

    var stack = new Stack<IEnumerator<T>>();

    stack.Push(subjects.GetEnumerator());

    while (stack.Count > 0)
    {
        var en = stack.Peek();
        if (en.MoveNext())
        {
            var subject = en.Current;
            yield return subject;

            stack.Push(selector(subject).GetEnumerator());
        }
        else 
        {
            stack.Pop();
        }
    }
}

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

Это создает то же число перечислителей, что и в рекурсивной версии, но, вероятно, будет меньше новых объектов, чем помещение всех объектов в очередь или стек и продолжение добавления любых объектов-потомков. Это время O(n), так как каждый перечислитель стоит сам по себе (в рекурсивной версии неявный вызов одного MoveNext исполняет MoveNext на дочерних перечислителях до текущей глубины в стеке рекурсии).

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

  HashSet<Person> GatherAll (Person p) {
     Stack<Person> todo = new Stack<Person> ();
     HashSet<Person> results = new HashSet<Person> ();
     todo.Add (p); results.Add (p);
     while (todo.Count > 0) {
        Person p = todo.Pop (); 
        foreach (Person f in p.Friends) 
           if (results.Add (f)) todo.Add (f);
     }
     return results;
  }

Это должно правильно обрабатывать циклы. Я начинаю с одного человека, но вы можете легко расширить это, чтобы начать со списка людей.

Вот реализация, которая:

  • Есть ли рекурсивный выбор глубины,
  • Не требует двойной итерации дочерних коллекций,
  • Не использует промежуточные коллекции для выбранных элементов,
  • Не обрабатывает циклы,
  • Можно сделать это задом наперед.

    public static IEnumerable<T> SelectRecursive<T>(this IEnumerable<T> rootItems, Func<T, IEnumerable<T>> selector)
    {
        return new RecursiveEnumerable<T>(rootItems, selector, false);
    }
    
    public static IEnumerable<T> SelectRecursiveReverse<T>(this IEnumerable<T> rootItems, Func<T, IEnumerable<T>> selector)
    {
        return new RecursiveEnumerable<T>(rootItems, selector, true);
    }
    
    class RecursiveEnumerable<T> : IEnumerable<T>
    {
        public RecursiveEnumerable(IEnumerable<T> rootItems, Func<T, IEnumerable<T>> selector, bool reverse)
        {
            _rootItems = rootItems;
            _selector = selector;
            _reverse = reverse;
        }
    
        IEnumerable<T> _rootItems;
        Func<T, IEnumerable<T>> _selector;
        bool _reverse;
    
        public IEnumerator<T> GetEnumerator()
        {
            return new Enumerator(this);
        }
    
        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    
        class Enumerator : IEnumerator<T>
        {
            public Enumerator(RecursiveEnumerable<T> owner)
            {
                _owner = owner;
                Reset();
            }
    
            RecursiveEnumerable<T> _owner;
            T _current;
            Stack<IEnumerator<T>> _stack = new Stack<IEnumerator<T>>();
    
    
            public T Current
            {
                get 
                {
                    if (_stack == null || _stack.Count == 0)
                        throw new InvalidOperationException();
                    return _current; 
                }
            }
    
            public void Dispose()
            {
                _current = default(T);
                if (_stack != null)
                {
                    while (_stack.Count > 0)
                    {
                        _stack.Pop().Dispose();
                    }
                    _stack = null;
                }
            }
    
            object System.Collections.IEnumerator.Current
            {
                get { return Current; }
            }
    
            public bool MoveNext()
            {
                if (_owner._reverse)
                    return MoveReverse();
                else
                    return MoveForward();
            }
    
            public bool MoveForward()
            {
                // First time?
                if (_stack == null)
                {
                    // Setup stack
                    _stack = new Stack<IEnumerator<T>>();
    
                    // Start with the root items
                    _stack.Push(_owner._rootItems.GetEnumerator());
                }
    
                // Process enumerators on the stack
                while (_stack.Count > 0)
                {
                    // Get the current one
                    var se = _stack.Peek();
    
                    // Next please...
                    if (se.MoveNext())
                    {
                        // Store it
                        _current = se.Current;
    
                        // Get child items
                        var childItems = _owner._selector(_current);
                        if (childItems != null)
                        {
                            _stack.Push(childItems.GetEnumerator());
                        }
    
                        return true;
                    }
    
                    // Finished with the enumerator
                    se.Dispose();
                    _stack.Pop();
                }
    
                // Finished!
                return false;
            }
    
            public bool MoveReverse()
            {
                // First time?
                if (_stack == null)
                {
                    // Setup stack
                    _stack = new Stack<IEnumerator<T>>();
    
                    // Start with the root items
                    _stack.Push(_owner._rootItems.Reverse().GetEnumerator());
                }
    
                // Process enumerators on the stack
                while (_stack.Count > 0)
                {
                    // Get the current one
                    var se = _stack.Peek();
    
                    // Next please...
                    if (se.MoveNext())
                    {
                        // Get child items
                        var childItems = _owner._selector(se.Current);
                        if (childItems != null)
                        {
                            _stack.Push(childItems.Reverse().GetEnumerator());
                            continue;
                        }
    
                        // Store it
                        _current = se.Current;
                        return true;
                    }
    
                    // Finished with the enumerator
                    se.Dispose();
                    _stack.Pop();
    
                    if (_stack.Count > 0)
                    {
                        _current = _stack.Peek().Current;
                        return true;
                    }
                }
    
                // Finished!
                return false;
            }
    
            public void Reset()
            {
                Dispose();
            }
        }
    }
    

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

public static IEnumerable<T> SelectRecursive<T>(this IEnumerable<T> subjects, Func<T, IEnumerable<T>> selector) {
    // Stop if subjects are null or empty 
    if (subjects == null || !subjects.Any())
        return Enumerable.Empty<T>();

    // Gather a list of all (selected) child elements of all subjects
    var subjectChildren = subjects.SelectMany(selector);

    // Jump into the recursion for each of the child elements
    var recursiveChildren = SelectRecursive(subjectChildren, selector);

    // Combine the subjects with all of their (recursive child elements).
    // The union will remove any direct parent-child duplicates.
    // Endless loops due to circular references are however still possible.
    return subjects.Union(recursiveChildren);
}

Это приведет к меньшему количеству дубликатов, чем ваш исходный код. Однако они могут быть дубликатами, вызывающими бесконечный цикл, объединение будет предотвращать только прямые дубликаты parent(s)-child(s).

И порядок вещей будет отличаться от вашего:)

Изменить: изменил последнюю строку кода на три оператора и добавил немного больше документации.

Используйте расширение Aggregate...

    List<Person> persons = GetPersons(); 
    List<Person> result = new List<Person>(); 
    persons.Aggregate(result,SomeFunc);

    private static List<Person> SomeFunc(List<Person> arg1,Person arg2)
    {
    arg1.Add(arg2)
    arg1.AddRange(arg2.Persons);
    return arg1;
    }

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

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

      class Node
{
    public readonly List<Node> Children = new List<Node>();

    public List<Node> Flatten()
    {
        var all = new List<Node>();
        Flatten(ref all);
        return all;
    }

    public void Flatten(ref List<Node> all)
    {
        all.Add(this);

        foreach (var child in Children)
            child.Flatten(ref all);
    }
}

Применение:

      Node rootNode = ...;
...
var all = rootNode.Flatten();
Другие вопросы по тегам