【问题标题】:Logic behind recursive functions that calls itself more than once in itself递归函数背后的逻辑,它本身不止一次调用自己
【发布时间】:2020-01-04 23:07:07
【问题描述】:

我理解只有一个嵌套函数的递归函数,比如这个:

def recurseInfinitely( n ):   
    try:
        recurseInfinitely(n+1)
    except RuntimeError:
        print("We got to level %s before hitting the recursion limit." % n)

这个函数调用自己直到遇到障碍,也就是 998 次。

但是当一个函数调用自己两次时,我真的很困惑。

这个问题的灵感来自 youtube 上的一个视频,该视频解释了递归并通过使用 Python 解决了一个名为河内之塔的问题。

Recursion 'Super Power' (in Python) - Computerphile

我把函数改成了这些:

def move(x, y, name):
    print("   {} Move from {} to {}".format(name, x,y))

def hanoi(n_of_disks,from_position,helper_position,target_position, name):
    if n_of_disks==0:
        print("#name: %s do nothing if there's no disk: %d" % (name, n_of_disks))
        pass
    else:
        # --> A
        print("Before A, name: %s disk: %d" % (name, n_of_disks))
        hanoi(n_of_disks-1,  # 3
            from_position,   # A
            target_position, # C
            helper_position, # B
            name='FIRST RECURSOR') 

        move(from_position,  # A
            target_position, # C
            name) 

        # --> B
        print("Before B, name: %s disk: %d" % (name, n_of_disks))
        hanoi(n_of_disks-1,  # 3
            helper_position, # B
            from_position,   # A
            target_position, # C
            name='SECOND RECURSOR') 

然后运行它...

hanoi(n_of_disks=4,
      from_position='A',
      helper_position='B',
      target_position='C',
      name="START THE SHOW")

结果是:

Before A, name: START THE SHOW disk left: 4
Before A, name: FIRST RECURSOR disk left: 3
Before A, name: FIRST RECURSOR disk left: 2
Before A, name: FIRST RECURSOR disk left: 1
#name: FIRST RECURSOR do nothing if there's no disk: 0
   FIRST RECURSOR Move from A to B
Before B, name: FIRST RECURSOR disk left: 1
#name: SECOND RECURSOR do nothing if there's no disk: 0
   FIRST RECURSOR Move from A to C
Before B, name: FIRST RECURSOR disk left: 2
Before A, name: SECOND RECURSOR disk left: 1
#name: FIRST RECURSOR do nothing if there's no disk: 0
   SECOND RECURSOR Move from B to C
Before B, name: SECOND RECURSOR disk left: 1
#name: SECOND RECURSOR do nothing if there's no disk: 0
   FIRST RECURSOR Move from A to B
Before B, name: FIRST RECURSOR disk left: 3
Before A, name: SECOND RECURSOR disk left: 2
Before A, name: FIRST RECURSOR disk left: 1
#name: FIRST RECURSOR do nothing if there's no disk: 0
   FIRST RECURSOR Move from C to A
Before B, name: FIRST RECURSOR disk left: 1
#name: SECOND RECURSOR do nothing if there's no disk: 0
   SECOND RECURSOR Move from C to B
Before B, name: SECOND RECURSOR disk left: 2
Before A, name: SECOND RECURSOR disk left: 1
#name: FIRST RECURSOR do nothing if there's no disk: 0
   SECOND RECURSOR Move from A to B
Before B, name: SECOND RECURSOR disk left: 1
#name: SECOND RECURSOR do nothing if there's no disk: 0
   START THE SHOW Move from A to C
Before B, name: START THE SHOW disk left: 4
Before A, name: SECOND RECURSOR disk left: 3
Before A, name: FIRST RECURSOR disk left: 2
Before A, name: FIRST RECURSOR disk left: 1
#name: FIRST RECURSOR do nothing if there's no disk: 0
   FIRST RECURSOR Move from B to C
Before B, name: FIRST RECURSOR disk left: 1
#name: SECOND RECURSOR do nothing if there's no disk: 0
   FIRST RECURSOR Move from B to A
Before B, name: FIRST RECURSOR disk left: 2
Before A, name: SECOND RECURSOR disk left: 1
#name: FIRST RECURSOR do nothing if there's no disk: 0
   SECOND RECURSOR Move from C to A
Before B, name: SECOND RECURSOR disk left: 1
#name: SECOND RECURSOR do nothing if there's no disk: 0
   SECOND RECURSOR Move from B to C
Before B, name: SECOND RECURSOR disk left: 3
Before A, name: SECOND RECURSOR disk left: 2
Before A, name: FIRST RECURSOR disk left: 1
#name: FIRST RECURSOR do nothing if there's no disk: 0
   FIRST RECURSOR Move from A to B
Before B, name: FIRST RECURSOR disk left: 1
#name: SECOND RECURSOR do nothing if there's no disk: 0
   SECOND RECURSOR Move from A to C
Before B, name: SECOND RECURSOR disk left: 2
Before A, name: SECOND RECURSOR disk left: 1
#name: FIRST RECURSOR do nothing if there's no disk: 0
   SECOND RECURSOR Move from B to C
Before B, name: SECOND RECURSOR disk left: 1
#name: SECOND RECURSOR do nothing if there's no disk: 0

一些问题:

  1. 为什么它的行为就像它的线程?
  2. 为什么一开始会从 4 倒数到 1,后来又这样?
  3. 为什么第一步不是从A到C,而是从A到B?
  4. 递归在现实生活中真的有用吗? 根据我的阅读,它是:

    • (-)资源昂贵
    • (-)慢
    • (+)更易于实施

【问题讨论】:

    标签: python recursion towers-of-hanoi


    【解决方案1】:

    我不确定理解这段代码的目的,但我确实理解递归。

    正如您所写,函数recurseInfinitely 是一个终端递归 函数。这意味着递归调用是函数中要完成的最后一个操作。因此这种函数的执行是线性的:

    recurseInfinitely(0)
    └─ recurseInfinitely(1)
       └─ recurseInfinitely(2)
          └─ ...
    

    相反,函数hanoi 不是终端递归的,因为它自己调用了两次。执行看起来像一棵二叉树。例如n = 3

    hanoi(3,A,B,C)           3
    ├─ hanoi(2,A,C,B)        2
    │  ├─ hanoi(1,A,B,C)     1
    │  │  ├─ hanoi(0,A,C,B)  0
    │  │  └─ hanoi(0,C,A,B)  0
    │  └─ hanoi(1,C,A,B)     1
    │     ├─ hanoi(0,C,B,A)  0
    │     └─ hanoi(0,A,C,B)  0
    └─ hanoi(2,B,A,C)        2
       ├─ hanoi(1,B,C,A)     1
       │  ├─ hanoi(0,B,A,C)  0
       │  └─ hanoi(0,C,B,A)  0
       └─ hanoi(1,A,B,C)     1
          ├─ hanoi(0,A,C,B)  0
          └─ hanoi(0,B,A,C)  0
    

    对应你的结果。

    在实践中,应避免使用这种算法,因为它会随着n (~2^n)指数增长。

    关于递归的有用性,递归更昂贵或更慢的说法是不正确的。然而,真正的情况是递归并不适合所有语言。事实上,有些语言完全基于递归(例如LispSchemeSQL)。它被称为函数式编程,确实非常优雅。

    例如,Scheme 中的阶乘函数可以写成

    (define (fact n)
        (if (zero? n)
            1
            (* n (fact (- n 1)))
        )
    )
    

    应该注意,这个函数不是终端递归的,因为它在递归调用之后进行乘法运算。

    它的终端版本(使用累加器)将是

    (define (fact n)
        (fact-acc n 1)
    )
    
    (define (fact-acc n acc)
        (if (zero? n)
            acc
            (fact-acc (- n 1) (* n acc))
        )
    )
    

    【讨论】:

    • hanoi 只是树遍历,所有树遍历都需要递归来完成。一个很好的例子是第一个 Fortran 编译器,它不能在 Fortran 中实现,因为它不支持递归。源代码解析具有非常递归的性质。
    • @Sylwester 确实,代码解析就是一个很好的例子!但是,可以使用队列/堆栈迭代地实现树遍历。
    • 第二次以这种方式引入堆栈,您正在执行递归过程,而 IMO 与编写递归函数并没有太大区别。请记住,CPU 没有 forwhile 循环或带参数的函数。这些功能都是由语言模拟的,所以我不知道为什么有人会建议使用递归函数,而带有堆栈的 while 循环则大不相同。
    【解决方案2】:

    我不能回答你所有的问题,但我可以回答:

    为什么第一步不是从A到C,而是从A到B?

    假设如果 n=2,第一步是按以下步骤将 A 移动到 B:

    FIRST RECURSOR Move from A to B
    START THE SHOW Move from A to C
    SECOND RECURSOR Move from B to C
    

    或者我们可以通过这些步骤将塔移到 B

    FIRST RECURSOR Move from A to C
    START THE SHOW Move from A to B
    SECOND RECURSOR Move from C to B
    

    我们已经证明了我们可以将顶部的两个块移动到任何位置。让我们考虑一下 n=3 的情况。我们可以将顶部的两个块移动到 B 或 C 中。但是,为了让 C 在第三块中保持空闲,我们需要将顶部的两个块移动到 B。如下所示:

    FIRST RECURSOR Move from A to C
    FIRST RECURSOR Move from A to B
    SECOND RECURSOR Move from C to B
    
    START THE SHOW Move from A to C
    FIRST RECURSOR Move from B to A
    SECOND RECURSOR Move from B to C
    SECOND RECURSOR Move from A to C
    

    再一次,我们可以将前三个块移动到 B 或 C,因为我们可以将前两个块移动到 B 或 C。这个逻辑递归地应用意味着对于每个奇数磁盘的河内塔的解决方案以 A 到 C 开头,偶数个磁盘有一个从 A 到 B 开头的解。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-08-16
      • 2016-12-06
      • 2016-04-05
      • 2015-01-09
      • 2021-07-20
      • 2012-08-22
      • 1970-01-01
      相关资源
      最近更新 更多