如何“展开”“递归”结构

时间:2010-01-06 10:39:00

标签: c# recursion ienumerable

不确定如何调用它,但是说你有一个看起来像这样的类:

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

6 个答案:

答案 0 :(得分:40)

我不相信LINQ内置了任何内容来做到这一点。

像这样递归地执行它会有问题 - 最终会创建大量的迭代器。如果树很深,这可能是非常低效的。 Wes DyerEric Lippert都有关于此的博文。

您可以通过删除直接递归来消除此低效率。例如:

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)那么你将永远递归

答案 1 :(得分:10)

我找到了这个问题,因为我一直在寻找并考虑类似的解决方案 - 在我的案例中,为ASP.NET UI控件创建了一个高效的IEnumerable<Control>。我所用的递归yield很快,但我知道这可能会产生额外的成本,因为控制结构越深,它就越长。现在我知道这是O(n log n)。

这里给出的解决方案提供了一些答案,但正如评论中所讨论的那样,它确实改变了顺序(OP不关心的顺序)。我意识到要保留OP给出的顺序,并且我需要,简单的Queue(如使用Jon)和Stack都不会起作用,因为所有父对象都将首先产生,然后任何他们之后的孩子(反之亦然)。

要解决此问题并保留订单,我意识到解决方案只是将Enumerator本身放在Stack上。要使用OP原始问题,它将如下所示:

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。 / p>

答案 2 :(得分:1)

使用聚合扩展程序......

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

答案 3 :(得分:1)

这是一个实现:

  • 深度首先是递归选择,
  • 不需要对子集合进行双重迭代,
  • 不对所选元素使用中间集合,
  • 不处理周期,
  • 可以倒退。

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

答案 4 :(得分:0)

你也可以使用这样的非递归方法:

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

这也应该正确处理周期。我从一个人开始,但您可以轻松地将其扩展为以人员列表开头。

答案 5 :(得分:0)

递归总是很有趣。也许您可以将代码简化为:

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

与原始代码相比,它会产生更少的重复。然而,它们可能仍然是重复的,导致无限循环,联盟只会阻止直接父(s)-child(s)重复。

项目的顺序与您的顺序不同:)

编辑:将最后一行代码更改为三个语句,并添加了更多文档。