【问题标题】:How to best traverse Children of Children's Children to an unknown depth? [duplicate]如何最好地将Children of Children's Children 遍历到未知深度? [复制]
【发布时间】:2013-08-17 00:17:43
【问题描述】:

想象一个具有属性的对象:

        class TestObject
        {
            public string Name { get; set; }
            public Collection<TestObject> Children { get; set; }
        }

现在以锯齿状方式初始化一些:

var person1 = new TestObject(){ 
                          Name = "Joe", 
                          Children = new Collection<TestObject>(){ childCollection1 }; 
                              };

var person2 = new TestObject(){ 
                          Name = "Mary", 
                          Children = new Collection<TestObject>(){ childCollection2 }; 
                              };

其中 Joe 的 childCollection 仅深一层,但 Mary 的孩子有孩子,他们也有孩子。

我尝试使用 SelectMany,但没有成功。

// Works
var joe = person1.Children.SelectMany(c => c.Children).Concat(person1.Children);

// Does not work - only returns 1 level deep
var mary = person2.Children.SelectMany(c => c.Children).Concat(person2.Children);

检索包含每个孩子的结果到未知深度的最佳方法是什么?

【问题讨论】:

  • 你的数据是如何初始化的?上面的代码不会创建超过一层深度的树。

标签: c# linq


【解决方案1】:

你可以这样写一个通用的遍历方法:

public static IEnumerable<T> Traverse<T>(T root, 
    Func<T, IEnumerable<T>> childSelector)
{
    var stack = new Stack<T>();
    stack.Push(root);

    while (stack.Any())
    {
        var next = stack.Pop();
        yield return next;
        foreach (var child in childSelector(next))
            stack.Push(child);
    }
}

这是一个通用模型,通常对遍历树很有用。请注意,这将进行深度优先搜索。如果您想先进行呼吸搜索,您可以使用Queue&lt;T&gt; 而不是Stack&lt;T&gt;

【讨论】:

  • 使用堆栈非常有趣 - 处理一些性能数据来比较两个答案。谢谢。
  • 在我的性能测试中,与 LINQ/递归方法相比,我的性能提高了 10-13%。虽然性能不是最初问题的一部分,但值得注意。
【解决方案2】:

辅助方法

public static IEnumerable<T> Traversal<T>(
    T root,
    Func<T, IEnumerable<T>> getChildren)
{
    if (root == null)
    {
        yield break;
    }

    yield return root;

    var children = getChildren(root);
    if (children == null)
    {
        yield break;
    }

    foreach (var child in children)
    {
        foreach (var node in Traversal(child, getChildren))
        {
            yield return node;
        }
    }
}

//Or if you don't need all those null checks, here's a more compact version.
public static IEnumerable<T> Traversal<T>(
    T root,
    Func<T, IEnumerable<T>> getChildren)
{
    yield return root;
    foreach (var child in getChildren(root))
        foreach (var node in Traversal(child, getChildren))
            yield return node;
}

//If you like a LINQ/functional style better, this is also equivalent.
public static IEnumerable<T> Traversal<T>(
    T root,
    Func<T, IEnumerable<T>> getChildren)
{
    return new T[] { root }
        .Concat(getChildren(root)
            .SelectMany(child => Traversal(child, getChildren)));
}

用法

var everybody = Traversal(person, x => x.Children);

评论

您可以轻松地修改Traversal 方法以完全按照您想要的方式运行。例如,如果您只想要叶节点,那么您应该只在children 为空或为空时使用yield return root;

性能问题

如果性能是任何类型的问题,请考虑上面的 LINQ/功能实现或查看 Servy 的答案,其中任何一个都应该比使用 yield ... 的版本更有效。

【讨论】:

  • 确实应该避免使用递归迭代器块。与传统方法不同,它们相当“重”,并且具有更多相关的状态和与之相关的构造/破坏。相反,编写一个非递归遍历函数将有相当显着的性能改进。
  • @Servy Point 很好。但是,OP 可能对这里的性能完全不感兴趣。我个人会从这个实现开始,只有当我发现它太慢时才改变它。
  • 不同之处在于这不是一次性使用的功能;它被设计成可以在很多地方使用的通用产品。这更有可能值得优化。优化实际上使方法更简单、更容易使用、更灵活,而且性能更高,更容易做出决定。
  • @Servy 优化并没有使方法更简单。您的方法可能看起来更简单的原因是因为我正在执行的 null 检查在您的方法中不存在。如果删除了null 检查,我的可以写成 4 行。看看编辑后的答案。
  • @Servy 另外,我上面的第三个实现(使用函数式 LINQ)应该更有效。
猜你喜欢
  • 1970-01-01
  • 2019-08-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-11-29
  • 2014-07-06
  • 2012-09-12
相关资源
最近更新 更多