Jak "rozwinąć" strukturę "rekurencyjną"

Nie wiem, jak to nazwać, ale powiedzmy, że masz klasę, która wygląda tak:

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

Następnie masz osobę i chcesz" rozwinąć " tę strukturę rekurencyjnie, aby skończyć z pojedynczą listą wszystkich ludzi bez duplikatów.

Jak byś to zrobił? Zrobiłem już coś, co wydaje się działać, ale jestem ciekaw, jak inni to zrobią, a zwłaszcza jeśli jest coś wbudowanego w Linq, którego możesz użyć w sprytny sposób, aby rozwiązać ten mały problem :)

Oto moje rozwiązanie:

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

Można by użyć czegoś takiego:

var people = somePerson.SelectRecursive(x => x.Friends);
Author: John Saunders, 2010-01-06

6 answers

Nie wierzę, że jest coś wbudowanego w LINQ, aby to zrobić.

Jest problem z robieniem tego rekurencyjnie w ten sposób - w końcu tworzy się dużą liczbę iteratorów. Może to być dość nieefektywne, jeśli drzewo jest głębokie. Wes Dyer i Eric Lippert obaj blogowali o tym.

Możesz usunąć tę nieefektywność, usuwając bezpośrednią rekurencję. Na przykład:

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

To również zmieni kolejność iteracji - zamiast tego staje się szersza głębokość-pierwszy; przepisanie go nadal być głębokość-pierwszy jest trudne. Zmieniłem również, aby nie używać Any() - ta poprawiona wersja nie będzie oceniać żadnej sekwencji więcej niż raz, co może być przydatne w niektórych scenariuszach. To ma jeden problem, pamiętaj - zajmie więcej pamięci, ze względu na kolejkowanie. Prawdopodobnie moglibyśmy to złagodzić, przechowując kolejkę iteratorów zamiast elementów, ale nie jestem pewien, czy jest to możliwe... to z pewnością byłoby bardziej skomplikowane.

Jeden punkt do odnotowania (również odnotowany przez ChrisW podczas gdy szukałem postów na blogu :) - jeśli masz jakieś cykle na liście znajomych (tzn. jeśli A ma B, A B ma a), to będziesz rekurencyjnie w nieskończoność.

 39
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-06 10:50:07

Znalazłem to pytanie, ponieważ szukałem i myślałem o podobnym rozwiązaniu - w moim przypadku stworzenie skutecznego IEnumerable<Control> dla ASP.NET sterowanie UI. Rekurencyjny yield, który miałem, jest szybki, ale wiedziałem, że może to mieć dodatkowy koszt, ponieważ im głębsza struktura kontrolna, tym dłużej może to potrwać. Teraz wiem, że to jest O (n log n).

Podane tutaj rozwiązanie daje pewną odpowiedź, ale, jak wspomniano w komentarzach, zmienia kolejność (o czym OP nie dbał). Zdałem sobie sprawę, że do zachowaÄ ‡ kolejnoĹ "Ä ‡ podanÄ ... przez OP i tak jak potrzebowaĺ' em, ani proste Queue (Jak uĺźyĺ 'Jon), ani Stack nie dziaĹ' aĹ 'y, poniewaĹź wszystkie obiekty rodzicä ... byĹ' y najpierw przekazywane, a nastÄ ™ pnie dzieci (lub vice-versa).

Aby to rozwiązać i zachować porządek zdałem sobie sprawę, że rozwiązaniem byłoby po prostu umieścić Enumerator się na Stack. Aby użyć oryginalnego pytania OPs, wyglądałoby to tak:

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

Używam stack.Peek tutaj, aby nie zmuszać tego samego enumeratora do stos, ponieważ jest to prawdopodobnie częstsza operacja, oczekując, że enumerator dostarczy więcej niż jeden element.

Tworzy to taką samą liczbę enumeratorów jak w wersji rekurencyjnej, ale prawdopodobnie będzie mniej nowych obiektów niż umieszczenie wszystkich obiektów w kolejce lub stosie i kontynuowanie dodawania obiektów potomnych. Jest to czas O(n), ponieważ każdy enumerator stoi na swoim (w wersji rekurencyjnej niejawne wywołanie do one MoveNext wykonuje MoveNext na potomnych enumeratorach do aktualna głębokość stosu rekurencyjnego).

 10
Author: Kevin Brock,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-06-08 11:33:20

Użyj rozszerzenia zbiorczego...

    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;
    }
 1
Author: Chen Kinnrot,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-06 13:59:19

Oto implementacja, która:

  • wykonuje najpierw rekurencyjną selekcję głębi,
  • nie wymaga podwójnej iteracji kolekcji potomnych,
  • nie używa zbiorów pośrednich dla wybranych elementów,
  • nie obsługuje cykli,
  • Można to zrobić od tyłu.
    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();
            }
        }
    }
    
 1
Author: Brad Robinson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-04 03:51:54

Możesz też użyć metody nie rekurencyjnej takiej jak Ta:

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

To powinno obsługiwać cykle prawidłowo, jak również. Iam zaczyna się od jednej osoby, ale można to łatwo rozszerzyć, aby rozpocząć od listy osób.

 0
Author: Tarydon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-06 10:54:39

Rekurencja jest zawsze zabawna. Być może mógłbyś uprościć swój kod do:

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

Spowoduje to mniej duplikatów niż oryginalny kod. Jednak nadal mogą być duplikatami powodującymi niekończącą się pętlę, Unia zapobiegnie tylko bezpośrednim duplikatom rodzica-dziecka.

A kolejność przedmiotów będzie inna niż Twoja:)

Edit: zmienił ostatnią linię kodu na trzy instrukcje i dodał nieco więcej dokumentacji.

 0
Author: Zyphrax,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-01-07 10:42:36