有人可以逐步解释此功能的作用吗

时间:2019-12-08 01:01:23

标签: python recursion

def hanoi(n,f,v,t):
    if n == 0:
        pass
    else:
        hanoi(n-1,f,t,v)
        print(f"Move disc from {f} to {t}.")
        hanoi(n-1,v,f,t)

hanoi(3,"a","b","c")

我正在研究递归,无法弄清楚这个“河内之塔”功能。它工作正常,但我不知道它在做什么。例如,函数打印的指令之一是:
“将光盘从c移到b。”
但是在我看来{f}只能是“ a”或“ b”?
何时(n = 2)似乎很容易理解,因为它调用(n = 1)打印“将光盘从a移动到b”。
然后移回(n = 2),打印“将光盘从a移到c”。
然后调用(n = 1),打印“将光盘从b移到c”。
但是我不明白n值越大的情况。

4 个答案:

答案 0 :(得分:2)

这有助于首先用伪代码表达问题:

move_tower(height_of_tower, "origin", "destination", "helper"):
    if height_of_tower is 1:
        move disk directly from "origin" to "destination"
    else:
        move height_of_tower-1 disks from "origin" to "helper" using "destination"
        move remaining single disk from "origin" to "destination"
        move height_of_tower-1 disks from "helper" to "destination" using "origin"

此伪代码可以立即转换为Python代码。您使用n = 0的基本情况只是另一种写法,因为对于n = 1,else块中的两个函数调用都不会执行任何操作(基本情况n = 0只是{{1 }}。

递归函数具有两个重要的属性: 1.首先,它定义了一个带有简单解的基本案例,该基本案例直接给出。 2.否则,它说明了如何从局部解中获得解;为此,可以使用任何函数,包括递归函数本身。唯一重要的是,它使用减少的输入数据进行调用,以便在某些时候命中基准。

关于您的问题:“但是在我看来{f}只能是“ a”或“ b”?……但是我不明白n的较大值会发生什么。”

否,{f}根据情况成为“原点”,“目的地”和“助手”标尺。它可以帮助将函数调用写下一个小的n,例如n = 3,并绘制递归树(一个流行的例子是recursive Fibonacci function)。例如,如果n = 3,则会发生这种情况:

  1. move_tower(2,“ origin”,“ helper”,“ destination”)=将2个磁盘从“ origin”转移到“ helper”(您已经知道它的工作原理了吧?)
  2. 将剩余磁盘直接从“源”移动到“目的地”
  3. move_tower(2,“ helper”,“ destination”,“ origin”)=使用“ origin”作为助手将2个磁盘从“ helper”(在步骤1中放入它们)转移到“ destination”

通常,很难真正地“递归思考”。它有助于将您的函数视为常规函数,可以使用任何其他函数来获得所需的结果,包括其自身,但数据集会减少。基本情况可确保它不会永远运行。

答案 1 :(得分:1)

第1行:河内输入n,f,v,t

2:如果n为0,则不做任何事情

3:否则,

4:运行n设为n-1的河内

5:使用f打印一些内容

6:在切换输入的情况下运行河内

7:

8:按照以下步骤运行河内

这只是在这里说明代码以帮助您,而不是告诉您它的确切作用。

这与您提到的河内塔功能有关,here是更详细的信息。它是递归的,因为它在其内部运行函数。

答案 2 :(得分:1)

以3个磁盘Word.run(function (context) { var range = context.document.getSelection(); var tables = context.document.body.tables.load("items"); var topBorders = []; return context.sync() .then(function () { for (var i = 0; i < tables.items.length; i++) { topBorders.push(tables.items[i].getBorder("Top").load(['color', 'width'])); } }) .then(context.sync) .then(function () { for (var i = 0; i < topBorders.length; i++) { range.insertText(topBorders[i].color.toString(), "End"); range.insertText(topBorders[i].width.toString(), "End"); } }) .then(context.sync) }); 为例,恕我直言,最好的解释是在this blog的图片中:

按图片说明所有步骤(从0到7)。

对于未知的n值,当您具有以下条件时,可能会使用此函数:

  • n个有序对象从原点X移到目的地Y,且顺序保持不变。
  • 一次只有3个杆就只能移动一个物体。

答案 3 :(得分:1)

要了解递归,您必须先了解递归。

Imo,将您的头放在任何递归函数上的最好方法是写下(以书面形式,老式风格)某种形式发生的事情。

为什么要用纸?绘制随机的东西比在计算机上容易和快捷。这很简单,但是递归更为复杂,因此将所有内容显式写出可能不切实际。在那种情况下,我喜欢象征性地总结内容,绘制图表等。

第二个最好的例子是使它打印东西。为此,让我们稍微修改一下您的初始代码。

def hanoi(n,_from,,t):
    print(f"Hanoi called: n:{n}, f:{f}, v:{v}, t:{t}")
    if n == 0:
        pass
    else:
        hanoi(n-1,f,t,v)
        print(f"Move disc from {f} to {t}. n:{n}")
        hanoi(n-1,v,f,t)

hanoi(3,"a","b","c")

输出结果:

Hanoi called: n:3, f:a, v:b, t:c (4)
Hanoi called: n:2, f:a, v:c, t:b (2)
Hanoi called: n:1, f:a, v:b, t:c (1)
Move disc from a to c. n:1       (1)
Move disc from a to b. n:2       (2)
Hanoi called: n:1, f:c, v:a, t:b (3)
Move disc from c to b. n:1       (3)
Move disc from a to c. n:3       (4)
Hanoi called: n:2, f:b, v:a, t:c (6)
Hanoi called: n:1, f:b, v:c, t:a (5)
Move disc from b to a. n:1       (5)
Move disc from b to c. n:2       (6)
Hanoi called: n:1, f:a, v:b, t:c (7)
Move disc from a to c. n:1       (7)

Process finished with exit code 0

如果您将此与此image

进行比较

请注意,不满足基本条件(n == 0)的第一个调用如何继续“深入”到递归中。达到基本情况后,递归将取消调用的堆栈-您从LAST递归调用中移出(n == 1,第一个移动磁盘)。然后n == 2(第二个移动磁盘)等...

也许并不那么具有讽刺意味,河内的塔楼本身是一个很好的类比递归的工作方式:您实际上将调用(例如,您最终要做的事情)堆叠成一大堆。然后,一旦您完成了新调用的堆积(例如,您已经达到基本情况,此处n == 0),那么您将接听最后放入您的调用堆栈中的所有调用并执行。

编辑:我已经删除了对河内的n == 0调用(因为它们什么也不做,只会使事情变得混乱)。我在括号中添加了与图像上的每个调用(2),(3)等相对应的图像中的步骤。每个#将出现两次-执行一次(例如,“从磁盘移动磁盘...”),然后一次放入调用栈(“ Hanoi调用...。”)。

如您所见,当我们打给河内一个n> 1的电话时,其中一些电话会堆叠在一起(例如(1)(2)(4))。当我们达到n == 0时,我们将逐步将它们堆叠。因此,我们首先执行-> c,即(1),因为该调用最后被堆叠到了递归堆栈上。例如。 (4)首先添加了n == 3,但是在执行它之前,递归迫使我们堆叠另一个n == 2(2)和n == 1(1)的对象。只有到那时,我们才能开始执行对河内的积压呼叫。我们将了解为什么对河内的第一次呼叫在下面标记为(4)而不是(3)。只要确保您正确理解了第一部分即可。

然后我们马上执行a ---> b(即(2))。但是在那一点上n == 2,所以当我们对河内进行该调用时,在继续进行(4)之前,我们在堆栈(3)上添加了另一个调用。由于最后一个被添加,我们立即拆开(3)的堆栈。那么,我们剩下的唯一要从调用堆栈弹出的调用是(4)。由于其n == 3,这将使我们在递归堆栈(6)和(5)上添加更多新调用。同样,我们以相反的顺序拆栈它们(因为(5)在(6)的“顶部”)。使用(6),n == 2,因此我们将向河内添加另一个调用,将是(7)。那时,我们不再向堆栈添加调用,因此我们完全摆脱了递归。