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

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

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);

Ответ 1

Я не верю, что в LINQ ничего не было сделано для этого.

Есть проблема с рекурсивно подобным образом: вы создаете большое количество итераторов. Это может быть весьма неэффективно, если дерево глубокое. Wes Dyer и Эрик Липперт об этом сообщают в блоге.

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

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

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

Ответ 2

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

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

Чтобы решить эту проблему и сохранить заказ, я понял, что решение просто состоит в том, чтобы поставить Enumerator на Stack. Чтобы использовать исходный вопрос OPs, он будет выглядеть следующим образом:

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 для дочерних счетчиков до текущей глубины в стеке рекурсии).

Ответ 3

используйте расширение 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;
    }

Ответ 4

Здесь реализована реализация, которая:

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

    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();
            }
        }
    }
    

Ответ 5

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

  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;
  }

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

Ответ 6

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

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);
}

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

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

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