我有一个如下方法,它搜索一个集合并递归地评估一个条件:
public static bool Recurse(this INodeViewModel node, Func<INodeViewModel,bool> predicate)
{
INodeViewModel currentNode = node;
return predicate(currentNode) || node.Children.Select(x => Recurse(x, predicate)).Any(found => found);
}
或者,这可以使用堆栈来实现,以避免递归,如下所示:
public static bool UsingStack(this INodeViewModel node, Func<INodeViewModel, bool> predicate)
{
var stack = new Stack<INodeViewModel>();
stack.Push(node);
while(stack.Any())
{
var current = stack.Pop();
if (predicate(current))
return true;
foreach (var child in current.Children)
{
stack.Push(child);
}
}
return false;
}
我的问题是,与递归版本相比,当树的深度较大时,堆栈版本是否提供了任何性能优势?
答案 0 :(得分:20)
我的问题是,与递归版本相比,当树的深度较大时,堆栈版本是否提供了任何性能优势?
是。当树的深度很大时,递归版本比迭代版本无限慢。那是因为递归版本会破坏调用堆栈,导致不可阻挡的堆栈空间异常,并在返回bool之前终止程序。在堆空间耗尽之前,迭代版本不会这样做,并且堆空间可能比堆栈空间大几千倍。
根本没有给出结果显然比在任何有限的时间内给出结果更糟糕。
但是,如果您的问题确实是“堆栈版本在树深处时提供任何好处,但不是那么深,以至于打击堆栈”,那么答案是:
您已经双向编写了该程序。运行它并找出答案。不要在两匹马的互联网图片上显示随机的陌生人并询问哪个更快;比赛他们然后你会知道。
另外:我倾向于通过编写遍历和产生每个元素的方法来解决您的问题。如果您可以编写方法IEnumerable<INode> BreadthFirstTraversal(this INode node)
和IEnumerable<INode> DepthFirstTraversal(this INode node)
,那么您不需要编写自己的搜索;你想搜索时可以说node.DepthFirstTraversal().Where(predicate).FirstOrDefault()
。
答案 1 :(得分:4)
让我们先说清楚:递归不是速度。它所做的任何事情都可以通过迭代以至少同样快的速度,通常更快的速度完成。递归的好处在于代码的清晰度。
话虽如此,除非你绝对需要最快的代码(坦率地说,你几乎从不这样做),第二个(数据递归)版本甚至不值得考虑,因为它增加了复杂性没有充分理由。它在C#中尤其毫无价值,因为每个Stack
操作都涉及一个方法调用,并且消除递归主要是为了摆脱方法调用。您几乎可以肯定添加工作,强制方法调用运行时可以使用内置堆栈更有效地处理的内容。
Eric makes a reasonable point about stack overflows,但为了使问题成为一个问题,你需要一个数千个节点深的树,或者你必须从已经很深的调用堆栈中搜索,或者谓词需要本身是递归的(可能通过触发其他搜索)。使用均匀略微平衡的树和不会导致更多递归的谓词,堆栈深度不应成为问题;默认堆栈已经足够大,可以处理相当多的递归,如果需要可以做得更大。
所有 表示,但是:我猜测,就像你一样,每个人都没有真正实现并测试过这两个版本。如果你在乎那么多,请花时间。
答案 2 :(得分:0)
第二个版本有几个优点:
您可以使用队列而不是堆栈轻松地从DFS切换到BFS。
如果深度太大,它将抛出一个可以处理的OutOfMemoryException。 (我相信会自动重新抛出StackOverflowException。)
性能和内存使用可能会更好,因为递归方法会在调用堆栈上保存所有局部变量(包括编译器生成的)。