在C#中使用递归

在使用递归来避免堆栈溢出时是否有任何一般规则?

你有多少次可以归还取决于:

  • 堆栈大小(通常是1MB IIRC,但二进制文件可以手工编辑;我不建议这样做)
  • 递归的每个级别使用多少堆栈(例如,具有10个未捕获的Guid局部变量的方法将比不具有任何局部变量的方法占用更多堆栈)
  • 您正在使用的JIT – 有时JIT 使用尾递归,有时则不会。 规则很复杂,我记不住了。 ( David Broman从2007年开始发布博客文章, 同一作者/日期的MSDN页面 ,但它们现在可能已经过时了。)

如何避免堆栈溢出? 不要过分推算:)如果你不能合理地确定你的递归会在没有走得很远的情况下终止(我会担心“超过10”虽然这是非常安全的)然后重写它以避免递归。

这实际上取决于你正在使用的递归算法。 如果它是简单的递归,你可以这样做:

 public int CalculateSomethingRecursively(int someNumber) { return doSomethingRecursively(someNumber, 0); } private int doSomethingRecursively(int someNumber, int level) { if (level >= MAX_LEVEL || !shouldKeepCalculating(someNumber)) return someNumber; return doSomethingRecursively(someNumber, level + 1); } 

值得注意的是,这种方法仅在将递归级别定义为逻辑限制时才有用。 如果不能发生这种情况(例如分而治之算法),则必须决定如何平衡简单性与性能与资源限制之间的关系。 在这些情况下,一旦达到arbritrary预定义限制,您可能必须在方法之间切换。 我在快速排序算法中使用的有效方法是将其作为列表总大小的比例。 在这种情况下,逻辑限制是条件不再是最佳时的结果。

我不知道有任何难以避免的堆栈溢出。 我个人试图确保 –
我的基本情况是正确的。
2.代码在某个时刻到达基本情况。

如果您发现自己生成了很多堆栈帧,则可能需要考虑将递归展开为循环。

特别是如果您正在进行多级递归(A-> B-> C-> A-> B …),您可能会发现可以将其中一个级别提取到一个循环中并为自己节省一些内存。

在连续呼叫之间的堆栈上留下的正常限制(如果不是很多)大约是15000-25000级别。 如果您使用IIS 6+,则为25%。

大多数递归算法可以迭代表达。

有多种方法可以增加分配的堆栈空间,但我宁愿让你先找到一个迭代版本。 🙂

除了拥有合理的堆栈大小并确保你分开并克服你的问题,以便你不断处理一个小问题,而不是真的。

我只想到了尾递归,但事实certificate,C#不支持它。 但是.Net-Framework似乎支持它:

http://blogs.msdn.com/abhinaba/archive/2007/07/27/tail-recursion-on-net.aspx

如果您在默认CLR下运行,则线程的默认堆栈大小为1 MB。 但是,其他主机可能会改变它。 例如,ASP主机将默认值更改为256 KB。 这意味着您可能拥有在VS下运行良好的代码,但在将其部署到真实托管环境时会中断。

幸运的是,当您使用正确的构造函数创建新线程时,您可以指定堆栈大小。 根据我的经验,这很少是必要的,但我已经看到一个案例,这就是解决方案。

您可以编辑二进制文件的PE标头以更改默认大小。 如果要更改主线程的大小,这非常有用。 否则我建议在创建线程时使用适当的构造函数。

我在这里写了一篇关于此的短文。 基本上,我传递了一个名为depth的可选参数,每次深入时都会向它添加1。 在递归方法中,我检查值的深度。 如果它大于我设置的值,我抛出exception。 值(阈值)取决于您的应用程序需求。

请记住,如果你不得不询问系统限制,那么你可能正在做一些可怕的错误。

因此,如果您认为在正常操作中可能会出现堆栈溢出,那么您需要考虑针对该问题的不同方法。

将递归函数转换为迭代函数并不困难,尤其是当C#具有Generic :: Stack集合时。 使用Stack类型将使用的内存移动到程序堆中而不是堆栈中。 这为您提供了存储递归数据的完整地址范围。 如果这还不够,那么将数据分页到磁盘并不困难。 但是如果你进入这个阶段,我会认真考虑其他解决方案。