【问题标题】:Issues with Recursion using Python 3使用 Python 3 的递归问题
【发布时间】:2015-03-09 14:01:23
【问题描述】:

我的问题是递归工作时 python 在做什么。我明白了这个概念,但似乎循环中的明确内容隐含在递归算法中。我已经看到了递归将循环然后退后一步以获得答案的示例。我不明白。就像发生了我没有写的代码一样。

我忍不住“看到”return 语句返回一个方程式,而不是构建一个方程式并返回答案。

有一些递归示例很有意义,但斐波那契和阶乘类型算法令人困惑。 (免责声明:我不想要斐波那契或阶乘的课程^_^。)

def main():
    num = int(input("Please enter a non-negative integer.\n"))
    fact = factorial(num)
    print("The factorial of",num,"is",fact)

def factorial(num):
    if num == 0:
        return 1
    else:
        return num * factorial(num - 1)

main()

如果我们这样做!10 我不禁认为它应该返回每个方程的结果并循环它。我不确定python是如何在内存中解决这个问题的。或者它如何知道它需要返回 10*9*8*7*6... 等的值

而不是返回 返回 10 * (10 - 1) 返回 9 * (9 - 1) 返回 8 * (8 - 1)

我知道 return 调用了函数,所以它不能返回任何东西......但是它如何处理它已经找到的值而不覆盖变量并丢失它的位置?

它是直接盯着我的脸,还是有什么我不知道的?

【问题讨论】:

标签: python recursion


【解决方案1】:

把它想象成一道数学题。如果您知道!9 的答案,您将如何计算!10?您只需将 !9 的值乘以 10。

这正是递归函数正在做的事情;它只是将num 的阶乘表达为与num 乘以num - 1 的阶乘相同。唯一不起作用的数字是0,但0 的阶乘是已知的,它是1

所以,10的阶乘基本上是:

  • 10 * factorial(9) ==
  • 10 * 9 * factorial(8) ==
  • 10 * 9 * 8 * factorial(7) ==
  • 10 * 9 * 8 * 7 * factorial(6) ==
  • 10 * 9 * 8 * 7 * 6 * factorial(5) ==
  • 10 * 9 * 8 * 7 * 6 * 5 * factorial(4) ==
  • 10 * 9 * 8 * 7 * 6 * 5 * 4 * factorial(3) ==
  • 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * factorial(2) ==
  • 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * factorial(1) ==
  • 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1 * factorial(0) ==
  • 10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1 * 1

请注意,每个factorial() 调用都会获得一组新变量。不会发生覆盖;这是对函数的全新调用。每次调用中num 的值完全独立于对函数的所有其他调用。

如果有帮助,请尝试使用笔记本跟踪函数信息。在页面上写下变量,并在您手动执行代码时更新它们。对于每一个新的函数调用,翻页并从下一张纸开始,在那里写下变量。你会在第一页写10,然后在第二页写9 (num - 1),等等。返回意味着获取返回的值,从笔记本中取出页面然后继续返回笔记本中的一个页面,以使用该返回值更新那里的变量。

Python 做同样的事情,使用frame 对象来跟踪变量。每个函数调用都是一个新帧,当函数返回时,帧会再次被丢弃。该调用的所有变量都随框架消失了。

另外,Python 并不关心您在此处重复使用相同的函数。您可以创建 11 个单独的函数,每个函数都有一个单独的名称和一个单独的 num 名称:

def factorial10(num10):
    if num10 == 0:
        return 1
    else:
        return num10 * factorial9(num10 - 1)

def factorial9(num9):
    if num9 == 0:
        return 1
    else:
        return num9 * factorial8(num9 - 1)

def factorial8(num8):
    if num8 == 0:
        return 1
    else:
        return num8 * factorial7(num8 - 1)

# ...
# etc. all the way to

def factorial0(num0):
    if num0 == 0:
        return 1
    else:
        return num0 * factorialminus1(num0 - 1)

Python 不会发现这些函数与原始函数有任何区别。将执行完全相同的工作,但不是重用相同的函数,而是使用具有相同行为的不同函数对象。只是名字变了。

因此,递归只是将一系列函数调用链接在一起的一种巧妙方法。这些函数调用都是独立的,它们不关心其他函数的局部变量在做什么。 Python 不需要“知道”任何东西,它只需要为你执行函数,当它遇到另一个函数调用时,执行该函数调用并使用返回值。该函数是相同函数还是不同函数没有任何区别。

【讨论】:

  • 哈,我就是不明白。我把它写在纸上,因为我可以看到它。它被写了,哪儿也去不了。我明白你写的。我只是不明白什么时候 num == 10,第一次调用是有道理的,但是 num 怎么可能 == 9 然后是 8 和 7 和 6 和 5 等等,记住 5 和 6 和 7 和 8 和 9 和 10同一时间。
  • @JasonMacAnLighiche:10 - 1 是什么?这就是下一个函数调用看到num = 9的方式。
  • @JasonMacAnLighiche:每个函数调用的信息都存储在一个frame中。局部变量只是该框架上的另一条信息。当您调用任何其他函数时,Python 如何记住您的函数变量?
  • @JasonMacAnLighiche:拿一个笔记本,只需要笔和纸。为每个函数调用使用一个单独的页面。在第一页写num = 10。翻页,写num = 9。下一页,写num = 8。等等。这就是您可以跟踪单独函数调用的单独变量的方式。这正是 Python 所做的,它使用 笔记本中的单独页面。只有页面是虚拟的并称为框架。并且每次函数返回时,页面都会从笔记本中撕下,然后您会返回到上一页。
  • 我理解计算,这是局部变量空间的这个“框架”以及最终计算如何“10 * 9 * 8 * 7 * 6 * 5 * 4 * 3 * 2 * 1 * 1”由python在内存中构造,以在num == 0时返回最终结果。到目前为止,Scope从未被打扰过。我正在尝试合并这些概念,这就像试图将一个圆圈变成一个正方形哈哈。不过我会试试你说的!
【解决方案2】:

这是一个很难以完全权威的方式回答的问题——我认为不同的人对递归有不同的思考方式。 对递归的思考方式是强迫自己以一种非常严谨、抽象的方式思考函数是什么。函数只是一个映射。这在原则上很简单,但在实践中很容易忘记,特别是如果您习惯于用命令式术语思考——也就是说,将程序视为指令集。

暂时忘记指令,以最抽象的形式考虑阶乘函数:

X               Y
--------------------
0    ------>    1
1    ------>    1
2    ------>    2
3    ------>    6
4    ------>    24
5    ------>    120
6    ------>    720
...

暂时不用担心如何计算。想想抽象映射。现在让我们考虑如何制作这个函数的递归版本。我们需要什么?好吧,我们需要一个创建不同映射的函数——不是从[1, 2, 3, ...] 到阶乘的映射,而是从Y 的一个值到下一个值的映射。换句话说(现在使用小写):

x               y
--------------------
1    ------>    1
1    ------>    2
2    ------>    6
6    ------>    24
24   ------>    120
120  ------>    720
720  ------>    5040
...

现在让我们考虑如何计算this。立即出现问题:1 第一次映射到1,第二次映射到2。所以我们知道我们将不得不编写一个特殊情况来区分这两者。但是对于其他人来说,这很简单,对吧?只需将x 乘以其在列表中的位置即可。所以这意味着对于映射的所有这些部分,我们只需要知道两件事:x,以及列表中的position

def factorial_recurrence(x, position):
    return x * position

请注意,这个函数现在有两个参数,所以它实际上与上面的函数略有不同:

x,   p               y
------------------------
1    0    ------>    1
1    1    ------>    2
2    2    ------>    6
6    3    ------>    24
24   4    ------>    120
120  5    ------>    720
720  6    ------>    5040

这非常清楚地显示了我们如何区分1 的两个映射。现在我们只需要想出一种方法来获取位置信息。碰巧positionX 的值相同。所以一种简单的方法是使用循环。在这里,我们通过简单地将x 设置为1 并在1 而不是0 开始我们的循环来处理X == 0

def factorial(X):
    x = 1
    for position in range(1, X + 1):
        x = factorial_recurrence(x, position)
    return x

现在注意这里x的值被传递到factorial_recurrence,然后结果被保存为x

所以这里真正发生的是函数的输出被传递回函数。这是一个重大的揭示:

这就是递归

从某种意义上说,这已经是一种递归算法。只是这里的表示是由内而外的,函数还结合了递归过程之外的position信息。要明白我的意思,请看这个:

def even_factorial(X):
    x = 1
    for position in range(2, X + 1, 2):
        x = factorial_recurrence(factorial_recurrence(x, position - 1), position)
    return x

对于X 的每个偶数值,这给出了与factorial 相同的结果。 (对于X 的奇数值,它给出了X - 1 的结果。)而且我们不必止步于此。我们可以对X 的每三个值做同样的事情(为了清楚起见,打破嵌套):

def third_factorial(X):
    x = 1
    for position in range(3, X + 1, 3):
        x = factorial_recurrence(
                factorial_recurrence(
                    factorial_recurrence(
                        x, 
                        position - 2
                    ), 
                    position - 1
                ), 
                position
            )
    return x

现在每 4 次、每 5 次等执行相同的操作。如果你继续这个过程,那么对于任何给定的X,你最终会创建一个函数,它只会返回1,直到你通过X,然后当你通过X,你会得到X 的阶乘。

在这一点上,递归的诀窍是简单地意识到我们可以通过让factorial 调用自身来自动化将循环内翻的过程。每次调用factorial,它只是在最后一个调用中嵌套另一个factorial_recurrence——除非X为0,在这种情况下,它返回1,终止嵌套调用序列。

def factorial(X):
    if X == 0:
        return 1
    else:
        return factorial_recurrence(factorial(X - 1), X)

所以这是一种复杂的递归思维方式,但它的价值在于它非常清楚地显示了递归函数的抽象与它们在命令式代码中的具体实现之间的关系。

【讨论】:

  • 很棒的答案谢谢你,我不习惯用这些术语思考。我今天遇到了这个,我对函数式编程做得不多。我通常只使用一个循环,但我对它真的很感兴趣。我会继续努力……并重读这篇文章!
猜你喜欢
  • 2021-09-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-07-14
相关资源
最近更新 更多