【问题标题】:Recursive factorial is suspiciously fast递归阶乘速度快得令人怀疑
【发布时间】:2016-09-24 04:51:59
【问题描述】:

由于问题的复杂性很高,阶乘的递归计算应该很慢。 为什么我的基本实现不是很慢?我很好奇,因为这应该是一个糟糕方法的教科书示例。

是因为 C# 程序中的一些内部优化或缓存结果吗?

using System;
using System.Diagnostics;
using System.Numerics;

namespace FactorialRecursion
{
    class Program
    {
        static void Main(string[] args)
        {
            Stopwatch stopwatch = new Stopwatch();
            for (int i = 0; i < 4000; i++)
            { 
                stopwatch.Reset();
                stopwatch.Start();
                Factorial(i);
                stopwatch.Stop();
                Console.WriteLine($"{i}! = ({stopwatch.Elapsed})");
            }
            Console.ReadKey();
        }

        static BigInteger Factorial(BigInteger number)
        {
            if (number <= 1)
                return 1;
            return number * Factorial(number - 1);
        }
    }
}

结果在这里:

3990! = (00:00:00.0144319)
3991! = (00:00:00.0149198)
3992! = (00:00:00.0159502)
3993! = (00:00:00.0116784)
3994! = (00:00:00.0104608)
3995! = (00:00:00.0122931)
3996! = (00:00:00.0128695)
3997! = (00:00:00.0131792)
3998! = (00:00:00.0142510)
3999! = (00:00:00.0145544)

【问题讨论】:

  • 或者你有一个快速的处理器?
  • 1) 调试它,或 2) 打印出阶乘的值,您可以自己查看。
  • 对于输入 N,您正在执行 N 次乘法运算。为什么不快呢?
  • “我很好奇,因为这应该是教科书上一个糟糕方法的例子。”您是否可能将它与斐波那契混淆,其中非常幼稚的递归版本是 O(2^n) 时间复杂度?
  • 如果您想检查缓存结果的想法,请尝试反转您的循环参数:从 4000 开始并向后工作。如果缓存是“罪魁祸首”,您会看到第一种情况的时间很长,4000!,而其他情况的时间很短。

标签: c# recursion factorial


【解决方案1】:

阶乘计算中的递归方法被认为是不好的,因为堆栈内存消耗而不是因为速度。

如果你取一个足够大的数字,你会很快耗尽内存来计算阶乘,而不是你会很慢(假设你正在存储的内存 阶乘ed 值大到足以存储它)。

【讨论】:

  • 这是如何回答 OP 的问题的?
  • @Sylwester:OP 说递归方法被认为是不好的,但他不知道为什么。这就是这个答案的答案。递归方法的缺点在于内存消耗,而不是速度。
  • @Sylwester:OP 假设坏意味着慢(可能你也有)。糟糕的是,在递归阶乘计算的上下文中,意味着在迭代版本中可能是 O(1) 时内存消耗为 O(n)。
  • @Sylwester 答案是 OP 的假设是错误的,递归版本根本没有时间问题。他的幼稚版本可能只是空间复杂性问题。
  • @displayName 只是简单地说,它不是O(1)。这是O(N*log(N))。 OP 正在使用 BigInteger。而N! 的位数是O(N*log(N))。所以在递归的情况下,它的顺序是O(N^2*log(N))。运行时间也是一样的。但是N = 4000 对于这个二次算法来说并不是一个特别大的输入。
【解决方案2】:

使用递归和迭代的阶乘函数的时间复杂度是相同的,尽管递归使用额外的内存空间来将函数调用存储在堆栈中。但是,就时间复杂度而言,它是相同的情况是因为递归间接地充当循环......当递归尝试一次又一次地计算相同的值时,递归的时间复杂度变得比它的迭代对应物更糟,这在迭代中被避免了。(例如:- Fibonacci num gen using recursion没有记忆的最坏情况时间复杂度为 O(2^n),而迭代对应的最坏情况时间复杂度为 O(n)。

【讨论】:

    【解决方案3】:

    JIT 有可能正在使用尾递归优化,这有助于加快速度,see this for more information。虽然无法访问您的环境,但很难确定。

    给定的方法被认为是不利的,因为它使用大量堆栈内存来处理更大的阶乘。它与速度的关系不大,而与它消耗的内存量有关。但是,如果您知道尾递归将在那里发生(由 JIT 决定,而不是由您决定,所以不要指望它),那真的没关系。

    【讨论】:

    • 不这么认为。 C# 没有 TCO,即使它有这个特定代码也不会在尾部位置递归。
    • 根据该链接,C# 编译器不支持尾递归,但 JIT 支持,您可以或多或少地通过使用特定编译器选项强制它,否则由 JIT 决定。这让我觉得这是可能的,因为最后一行是递归调用,但很可能不是。由于我不确定,所以我想我会在其中添加免责声明。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-28
    • 1970-01-01
    • 2013-04-28
    • 2015-04-19
    • 2013-08-21
    相关资源
    最近更新 更多