我想对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#首选。
答案 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)为同一棵树产生不同的结果,那么为什么要考虑懒惰呢?所以我认为排序/遍历方案(甚至首先是深度优先与广度优先)应该是您的聚合器固有的,还是固定的树类型?不是外部的参数?