【问题标题】:Recursion, can't understand the output递归,看不懂输出
【发布时间】:2017-11-20 20:45:41
【问题描述】:

为什么我在输出中得到一个额外的 1 * 1 并且它有点倒退?有点递归的初学者,希望得到详细的答案。

class Program
{

    public static long Factorial(int n)
    {
        if (n == 0)
            return 1;

        Console.WriteLine("{0} * {1}", n, Factorial(n - 1));
        return n * Factorial(n - 1);    
    }
    static void Main(string[] args)
    {
       long a = 0;
        a = Factorial(3);
        Console.WriteLine(a);
    }
}

输出

1 * 1
2 * 1
1 * 1
3 * 2
1 * 1
2 * 1
1 * 1
6

【问题讨论】:

  • 我投票结束这个问题作为题外话,因为调试帮助:这段代码是如何工作的?
  • @danihp 这基本上是某人 (@yosu) 犯的错误,也可能发生在其他人身上。所以这甚至可以帮助其他人,有一个类似的问题。它并没有那么广泛,它只是调试帮助,它是一个关于递归的相当紧凑的问题。
  • @MetaColon 代码有很大的错误,因为这个输出没有意义。 OP 要求忽略此错误。这是家庭作业。 OP应该自己做。它们是双重递归,应该是简单的。
  • @danihp 我想一开始就与递归混淆是很正常的,所以寻求帮助也很正常。而且我看不到 OP 在哪里要求忽略错误?

标签: c# recursion


【解决方案1】:

您递归地调用函数两次,一次在输出中,然后在下一行再次调用。这就是输出混乱的原因,因为您从Console.WriteLine() 方法调用它,然后立即在return 语句中再次调用它。

此外,0 的阶乘是 1,所以我在 WriteLine() 方法中添加了一个小三元语句检查,以便输出在数学上是正确的。

Console.WriteLine("{0} * {1}", n, Factorial(n - 1));  // Calling it once
return n * Factorial(n - 1);    // Calling it again, oops!

这里有一个小调整会有所帮助:

public static long Factorial(int n)
{
    if (n == 0)
        return 1;
    Console.WriteLine("{0} * {1}", n, (n>1?n-1:n));
    return n * Factorial(n - 1);    
}

static void Main(string[] args)
{
   long  a = Factorial(3);
    Console.WriteLine(a);
}

产量

3 * 2
2 * 1
1 * 1 
6

【讨论】:

  • 问题要求详细解答。
【解决方案2】:

这是因为日志输出中有第二个阶乘循环:

Console.WriteLine("{0} * {1}", n, Factorial(n - 1));

这需要在打印任何内容之前计算 Factorial(n-1)。 因为递归的每一步现在都会触发 两个 递归,所以它比您预期的要复杂得多!

所以阶乘(3):

  • 开始记录“3 * Factorial(2)” -> 但需要计算 Factorial(2)
    • Factorial(2) 开始记录“2 * Factorial(1) -> 但需要计算 Factorial(1)
      • Factorial(1) 开始记录“1 * Factorial(0) -> 但需要计算 Factorial(0)
        • Factorial(0) 返回 1
      • 现在 Factorial(1) 可以打印 "1 * 1"(您的第一行输出。)
      • 现在 Factorial(1) 需要计算 Factorial(0) 的返回值...
        • Factorial(0) 返回 1
    • Factorial(2) 可以记录 "2 * 1"(您的第二行输出。)
    • Factorial(2) 现在需要计算 Factorial(1) 才能返回
      • Factorial(1) 开始记录“1 * Factorial(0) -> 但需要算出 Factorial(0)”
      • ......等等等等

如果您将其更改为:

Console.WriteLine("{0} * Factorial({1})", n, n - 1);

然后你会得到更像你期望的日志记录。

(如果您正在学习递归,那么使用调试器并查看程序流程如何进行以及为什么它会导致您看到的输出对您来说可能会很有趣。)

【讨论】:

  • 但是,现在您的日志记录方向错误(如 3 * 2;2 * 1;1 * 0),这可能会造成混淆。而且,更令人困惑的是,如果您只尊重您的日志,那么您将得到 1 * 0 的结果。
  • 取决于您所说的“错误”方向是什么意思!如果您对函数调用的顺序感兴趣,这是正确的方向 - 但同意这是有争议的。我提出的建议给出了输出 '1 * Factorial(0)' 而不是 '1 * 0' - 但同样,最清晰的日志记录取决于目的。
【解决方案3】:

你应该从记分板上复制你的作业。你必须改变:

    Console.WriteLine("{0} * {1}", n, Factorial(n - 1));

通过

    Console.WriteLine("{0} * {1}", n, n - 1);

这个错误导致错误的输出。

【讨论】:

    【解决方案4】:

    事实上,你的错误与递归无关。你的问题是,你调用了一个方法两次,而不是保存它的值。如果您不打印结果,您甚至不会意识到这一点。正如@JLK 指出的那样,你在这个 sn-p 中调用了两次:

    Console.WriteLine("{0} * {1}", n, Factorial(n - 1));
    return n * Factorial(n - 1);
    

    让我们单步执行代码,这样我们就可以理解输出了:

    Factorial (3);
    -> output 3 * {Factorial (2)}          // 3 * 2 (4)
       -> output 2 * {Factorial (1)}       // 2 * 1 (2)
          -> output 1 * {Factorial (0)}    // 1 * 1 (1)
             <- 1 
             Factorial (0)
          <- 2
          Factorial (1)
             -> output 1 * {Factorial (0)} // 1 * 1 (3)
             <- 1
             Factorial (0)
          <- 1
       <- 1
       Factorial (2)
       -> output 2 * {Factorial (1)}       // 2 * 1 (6)
          -> output 1 * {Factorial (0)}    // 1 * 1 (5)
             <- 1
             Factorial (0)
          <- 1
          Factorial (1)
             -> output 1 * {Factorial (0)} // 1 * 1 (7)
             <- 1
             Factorial (0)
          <- 1
       <- 2
    

    这可能有点令人困惑,但这对于递归来说是正常的。所以基本上你所要做的就是把代码sn-p稍微改一下:

    var factorial = Factorial (n - 1);
    Console.WriteLine("{0} * {1}", n, factorial);
    return n * factorial;
    

    那么输出是:

    1 * 1
    2 * 1
    3 * 2
    6
    

    如果您想了解有关递归的更多信息,我建议您使用this 站点。它与 C# 无关,但它是一个学习递归的好网站。如果您想在递归方面做很多事情,我无论如何都不建议使用 C#,因为它是一种命令式语言。对于递归,函数式语言更适合。如果您仍然想保持接近 C#,或者至少使用 .Net 框架,我建议使用 F#,它的功能相当于 C#。

    C# 和递归的主要问题是,C# 不支持Tail recursion。当您进行更深层次的递归调用(例如无限递归循环)时,这很容易导致 StackOverflows。这是因为每次调用函数时,调用函数的代码地址都会被压入堆栈。所以每次你进行递归调用时,代码地址都会被推送到堆栈中。因此,例如,如果您有此功能:

    void rec () => rec();
    

    在 C# 中调用 StackOverflow 不到一秒的时间。然而,尾递归至少部分解决了这个问题:如果递归调用是函数中的最后一次执行,它不会推送调用者的代码地址。所以在 F# 中,这将永远运行:

    let rec r () =
        r ()
    r ()
    

    (F#语法我就不深入了,网上教程已经够多了)。我说它只解决了部分问题,因为:

    let rec r () =
        r ()
        ()
    r ()
    

    再次导致 StackOverflow。我不知道有任何编程语言,但情况并非如此,因此您只需要习惯使用尾递归即可。

    您可以在this 帖子中阅读有关函数式/递归语言的更多信息。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-21
      • 2015-08-25
      相关资源
      最近更新 更多