深度优先搜索的递归方法与堆栈

时间:2013-03-28 14:23:28

标签: c#

我有一个如下方法,它搜索一个集合并递归地评估一个条件:

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;
}

我的问题是,与递归版本相比,当树的深度较大时,堆栈版本是否提供了任何性能优势?

3 个答案:

答案 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。)

性能和内存使用可能会更好,因为递归方法会在调用堆栈上保存所有局部变量(包括编译器生成的)。