如何在C#中折叠n-ary树

时间:2013-10-23 14:04:45

标签: c# f# tree fold catamorphism

我想对n-ary Tree数据结构进行折叠。 (折叠在Linq中也称为Aggregate)
我设法提出了一个有效的解决方案:

public static R Aggregate<T, R>(T node,
          Func<T, IEnumerable<T>> getChildren,
       Func<T, IEnumerable<R>, R> aggregator)
{
    var childResults = getChildren(node)
                      .Select(c => Aggregate(c, getChildren, aggregator));

    return aggregator(node, childResults);
}

getChildren是一个定义如何获取给定节点的子节点的函数。它必须为叶节点返回一个空的IEnumerable aggregator定义了如何使用当前节点及其子节点的结果来处理节点。

解决方案似乎有效,但存在一些问题:

  • 该算法是递归的,它会为深树打击堆栈 如何重写函数以防止堆栈溢出?

  • 算法很懒,但只是一种。
    例如如果aggregator仅使用子节点的Enumerable.First结果,则仅遍历树的最左侧分支。但是对于Enumerable.Last,遍历整个树,即使计算中只需要最右边的分支 如何让算法真正变得懒惰?

欢迎使用F#解决方案,但C#首选。

2 个答案:

答案 0 :(得分:1)

您可以使用显式堆栈遍历树,而不是递归,以避免消耗堆栈空间:

public static IEnumerable<T> Traverse<T>(
    this IEnumerable<T> source
    , Func<T, IEnumerable<T>> childrenSelector)
{
    var stack = new Stack<T>(source);
    while (stack.Any())
    {
        var next = stack.Pop();
        yield return next;
        foreach (var child in childrenSelector(next))
            stack.Push(child);
    }
}

如果您希望然后“向后”遍历,则只需在调用时调整子选择器,而不是调用Last而不是First

Traverse(root, node => nodes.Reverse());

答案 1 :(得分:0)

  • 你在遍历树时花了你的记忆,如果你想保存堆栈,而不是先深度先切换到广度,或者是一些适合你确切要求的树遍历技术。

  • 至于使其“正常”懒惰,从遍历中取消挂钩聚合器。只需先构建一个惰性遍历(按照你想要的顺序)并将其传递给聚合器。

此外,不太确定您的界面选择与您对懒惰的担忧有关。 Enumerable.First vs Enumerable.Last会根据提供者的变化(getChildren)为同一棵树产生不同的结果,那么为什么要考虑懒惰呢?所以我认为排序/遍历方案(甚至首先是深度优先与广度优先)应该是您的聚合器固有的,还是固定的树类型?不是外部的参数?