运行时异常,递归太深

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 的值。

【讨论】:

是的——但这就是重点。不要用任何语言学习糟糕的技术。如果该语言为您提供了一种无需使用递归的方式来做某事的方法,那就是更好地使用该语言。 触摸。我的最后一条评论是针对你的第二句话,但我明白你的意思:) 相反,递归调用很少是一种非常糟糕的做事方式。除非它们可以导致无限的堆栈增长(例如遍历任意大小的不平衡树),否则它们会变得糟糕吗?

以上是关于运行时异常,递归太深的主要内容,如果未能解决你的问题,请参考以下文章

logback运行时异常怎么记录

常见的运行时异常(uncheckedExcepiton)和非运行时异常(checkedExcepiton)

编译时异常与运行时异常的区别

样式(已回答递归和越界异常,这是新的)

Java运行时异常和非运行时异常

运行时异常和非运行时异常