如何"展开""递归"结构

Svi*_*ish 32 c# recursion ienumerable

不知道如何调用它,但是说你有一个类似下面的类:

class Person
{
    public string Name;
    public IEnumerable<Person> Friends;
}
Run Code Online (Sandbox Code Playgroud)

然后你有一个人,你想要递归地"展开"这个结构,所以你最终得到一个没有重复的所有人的列表.

你会怎么做?我已经做了一些似乎有用的东西,但我很想知道其他人会怎么做,特别是如果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;
    }
}
Run Code Online (Sandbox Code Playgroud)

将使用这样的东西:

var people = somePerson.SelectRecursive(x => x.Friends);
Run Code Online (Sandbox Code Playgroud)

Jon*_*eet 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这也将改变迭代顺序 - 它变为广度优先而不是深度优先; 重写它仍然是深度优先是棘手的.我也改变它不使用Any()- 这个修订版本不会多次评估任何序列,这在某些情况下可能很方便.这确实有一个问题,请注意 - 由于排队,它会占用更多内存.我们可以通过存储迭代器队列而不是项目来缓解这个问题,但我不确定是否...它肯定会更复杂.

需要注意的一点(ChrisW在我查阅博客文章时也注意到了:) - 如果你的朋友列表中有任何周期(即如果A有B,B有A),那么你将永远递归.

  • @Inquisitor:只有类型是可变的.否则,您可以使用`HashSet <T>`来存储您已经访问过的项目. (2认同)
  • @Eric:这很有可能......虽然你先得到它的深度*并且每个集合中的最后一个*所以它仍然与原始顺序不匹配:(再次,我相信它可以用更多的努力 - 但是我的大脑目前还没考虑过. (2认同)
  • 啊,是的,我完全明白你的意思.有趣的巧合,我刚刚检查了我们用来确定发出什么顺序类的递归算法,并想知道它是否可以迭代.使这个算法迭代有这个问题; 它并不完全是深度优先的,因为它反转了给定命名空间中的类的顺序.通过明智地使用Reverse()序列运算符来修复它应该很容易. (2认同)

Kev*_*ock 11

我找到了这个问题,因为我正在寻找并考虑类似的解决方案 - 在我的案例中创建一个高效IEnumerable<Control>的ASP.NET UI控件.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().Dispose();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

stack.Peek在这里使用以防止必须将相同的枚举器重新推送到堆栈,因为这可能是更频繁的操作,期望枚举器提供多个项目.

这会创建与递归版本相同数量的枚举器,但可能会比将所有主题放入队列或堆栈并继续添加任何后代主题更少的新对象.这是O(n)时间,因为每个枚举器都独立存在(在递归版本中,一个隐式调用在子枚举器上MoveNext执行MoveNext到递归堆栈中的当前深度).

  • 从堆栈中弹出枚举器后,应该放置它. (3认同)