运行时异常,递归太深
Posted
技术标签:
【中文标题】运行时异常,递归太深【英文标题】:Runtime exception, recursion too deep 【发布时间】:2011-05-05 15:16:59 【问题描述】:我将伪代码here 转换为 C#,并让它递归重复 10,000 次。但是我在9217
次之后得到一个 C# 运行时错误,*** Exception
。我怎样才能防止这种情况发生?
编辑如果对任何人有帮助,代码如下:
private double CalculatePi(int maxRecursion)
return 2 * CalculatePi(maxRecursion, 1);
private double CalculatePi(int maxRecursion, int i)
if (i >= maxRecursion)
return 1;
return 1 + i / (2.0 * i + 1) * CalculatePi(maxRecursion, i + 1);
double pi = CalculatePi(10000); // 10,000 recursions
EDIT2 所以每个人似乎都同意我需要将其转换为迭代...有人可以提供一些代码吗?我似乎无法编写任何有效的迭代代码......
编辑感谢 Paul Rieck 提供的这个答案,我对其进行了测试,并且有效:
private static double CalculatePi(int maxRecursion)
double result = 1;
for (int i = maxRecursion; i >= 1; i-- )
result = 1 + i / (2.0 * i + 1) * result;
return result * 2;
【问题讨论】:
编译器将在编译期间显示错误或警告。你得到一个 runtime 异常。你能发布确切的异常细节吗? 将迭代次数从 10000 更改为 51。您会得到相同的结果。 :) 不,实际上。我可以将迭代设置为 1 到 9216 之间的任何位置,而不会出现异常。 @TheAdamGaskins:你没有抓住重点。我的意思是你不需要使用 10000 次递归。 51 及以上的任何值都会给出相同的结果。最后的 9949 次递归是无用的,因为它们的贡献超出了double
的精度。
msdn.microsoft.com/en-us/library/s1ax56ch(v=VS.80).aspx 或者,您可以使用十进制,但您可能必须调整调用方以使用该值。有关更多信息,请参阅上面链接的参考。
【参考方案1】:
所以每个人似乎都同意我需要将其转换为迭代...有人可以提供一些代码吗?我似乎无法编写任何有效的迭代代码......
您是要一条鱼,还是要学习如何捕鱼?如果本练习的目的是学习如何将递归代码转换为迭代代码,那么仅仅获得答案并不能很好地为您服务。
要将递归代码转换为迭代代码,有很多方法可以做到这一点。在这种情况下,最简单的方法是简单地计算出模式。代码有什么作用?它计算:
(1 + 1 / (2.0 * 1 + 1)) *
(1 + 2 / (2.0 * 2 + 1)) *
(1 + 3 / (2.0 * 3 + 1)) *
(1 + 4 / (2.0 * 4 + 1)) *
(1 + 5 / (2.0 * 5 + 1)) *
(1 + 6 / (2.0 * 6 + 1)) *
(1 + 7 / (2.0 * 7 + 1)) *
...
(1 + 9999/ (2.0 * 9999+ 1)) *
1
现在你能写一个循环来计算它吗?当然。
double product = 1.0;
for (int i = 9999; i >= 0; --i)
product *= (1 + i / (2.0 * i + 1));
这是最简单的方法。但是有很多方法可以解决这个问题。
您可以使用聚合器。考虑“总”操作;这是一个聚合器的例子。您有一系列事物,并保持它们的运行总数,将结果累积到累加器中。标准查询运算符为您提供了一个聚合操作:
double seed = 1.0;
Enumerable.Range(0, 10000).Aggregate(seed,
(product, i) => product * (1 + i / (2.0 * i + 1))
或者,您可以保持算法递归,但通过以下方式消除堆栈溢出:
在堆上构建您自己的堆栈结构 定义虚拟机,用虚拟机语言编写程序,然后实现虚拟机以将其堆栈保留在堆上。 以继续传递样式重写您的程序;然后沿途的每一步都向调用者返回一个延续,调用者调用下一个延续;堆栈永远不会变深解释这些技术需要很长时间才能得到一个答案。我有一个由六部分组成的博客系列,介绍如何使用这些技术将递归程序转变为不消耗太多堆栈的程序;从这里开始阅读:
Link
【讨论】:
【参考方案2】:“c# 编译器”可以很好地编译它。但是,在运行时,您最终可能会遇到 *** 异常(在我的机器上,10,000 可以正常工作,但稍后会死掉)。
这不是“无限递归异常”——堆栈溢出正是它所说的;调用方法意味着将信息放入堆栈并在调用返回时将其删除。您有 10,000 次调用,但没有一个返回,因此堆栈已满并引发异常。
在这种特殊情况下,您可以通过删除递归并迭代运行来相当容易地解决这个问题。在一般情况下,可以通过迭代、尾递归优化和继续传递来消除递归。
这里是迭代样式的快速直接翻译:
private static double CalculatePi(int maxRecursion)
double result = 1;
for (int i = maxRecursion -1; i >= 1; i-- )
result = 1 + i / (2.0 * i + 1) * result;
return result * 2;
【讨论】:
您必须从 maxRecursion 中减去 1 才能获得与递归版本完全相同的结果。请参阅我的答案中的代码。【参考方案3】:不要使用递归。这是一个不应该递归的很好的示例,因为这会导致调用堆栈溢出。
您可以轻松地将其转换为非递归。
【讨论】:
【参考方案4】:你无法阻止它,函数调用自身太多次;调用堆栈可以达到的深度以及您的函数可以达到的深度是有限制的。
【讨论】:
【参考方案5】:了解其他人在这里所说的话——这是一个 Stack Overflow(哇!*** 上的一个 Stack Overflow 问题。这可能会导致堆栈......)
无论如何,每次调用你的递归方法时,它都会压栈,也就是说,它会将对自身的引用放入堆栈,这样当最后一次调用CalculatePi时,它可以展开所有的其余的电话。
堆栈不是无限资源。每次压栈都会占用一点内存,当你用完时,就会出现栈溢出。
【讨论】:
【参考方案6】:递归调用不是尾递归的,即使是,它也无济于事,因为 C# 编译器当前不优化尾递归调用。编辑:正如 Eric Lippert 和 Gabe 所指出的,即使在发出的 IL 中没有明确的尾调用指令,CLR 也可以选择生成尾调用。
-
最佳方法是将这种递归解决方案转变为迭代解决方案。
由于它几乎完成,一个快速的技巧可能是增加运行此方法的线程的堆栈大小。
请不要这样做。仅供娱乐:
static void Main()
Console.WriteLine(SafeCalculatePi(10000));
// Calculates PI on a separate thread with enough stack-space
// to complete the computation
public static double SafeCalculatePi(int maxRecursion)
// We 'know' that you can reach a depth of 9217 for a 1MB stack.
// This lets us calculate the required stack-size, with a safety factor
double requiredStackSize = (maxRecursion / 9217D) * 1.5 * 1024 * 1024 ;
double pi = 0;
ThreadStart ts = delegate pi = CalculatePi(maxRecursion); ;
Thread t = new Thread(ts, (int)requiredStackSize);
t.Start();
t.Join();
return pi;
【讨论】:
是否有链接器/编译器选项来设置初始线程堆栈大小? @gabe: ***.com/questions/1042345/… 某些版本的抖动确实优化了一些尾递归程序,仅供参考。但一般情况下你不能依赖它。 @Ani:C# 编译器不发出尾调用指令是正确的。这和什么有什么关系?您似乎认为未标记为尾调用的调用不允许生成为尾调用;我知道这个想法没有事实依据。抖动完全在它认为合适的优化方法的权利范围内,包括在优化生成正确程序时将返回转换为尾调用。 Ani:我写了一个小函数,它调用自己 100,000,000 次,并确认它在 x64 上运行到未调试的发布版本上完成。如果我切换到 x86、进行 Debug 构建或在调试器下运行,则会出现堆栈溢出。【参考方案7】:这显然不是无限递归,因为它在 10,000 级后停止,但这仍然不是最好的主意。
一万层堆栈是非常多的——堆栈并不是一个巨大的资源。您应该将其转换为迭代解决方案。
【讨论】:
【参考方案8】:由于您几乎可以运行该程序,您可以让它使用更少的堆栈空间。只需在发布模式而不是调试模式下运行,它应该可以工作。
【讨论】:
【参考方案9】:执行此操作的迭代代码很简单:
private double CalculatePi(int iterations)
double pi = 1.0;
for (int i = iterations - 1; i >= 1; i--)
pi = 1 + i / (2.0 * i + 1) * pi;
return pi * 2.0;
用法:
double pi = CalculatePi(51);
【讨论】:
【参考方案10】:这将是非递归变体:
private double CalculatePi(int maxRecursion)
return 2 * CalculatePi2(maxRecursion, 1);
private double CalculatePi2(int maxRecursion, int i)
double product = 1;
while (i < maxRecursion)
product = product * (1f + i / (2.0f * i + 1f));
i++;
return product;
【讨论】:
【参考方案11】:迭代版本:
public static double CalculatePi(int maxRecursion)
double result=1;
for(int i=maxRecursion-1;i>=1;i--)
result=1+i/(2.0*i+1)*result;
return result*2;
【讨论】:
【参考方案12】:递归函数调用几乎总是一种非常糟糕的处理方式。
我会简单地从头文件/数学库中获取 pi 的值。
【讨论】:
是的——但这就是重点。不要用任何语言学习糟糕的技术。如果该语言为您提供了一种无需使用递归的方式来做某事的方法,那就是更好地使用该语言。 触摸。我的最后一条评论是针对你的第二句话,但我明白你的意思:) 相反,递归调用很少是一种非常糟糕的做事方式。除非它们可以导致无限的堆栈增长(例如遍历任意大小的不平衡树),否则它们会变得糟糕吗?以上是关于运行时异常,递归太深的主要内容,如果未能解决你的问题,请参考以下文章