带你理解 Hanoi 汉诺塔递归算法

一. 由游戏引发的 Hanoi 问题

汉诺塔是根据一个传说形成的一个问题。汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着 64 片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

二. 一种数学问题

带你理解 Hanoi 汉诺塔递归算法_第1张图片

我们将 Hanoi 问题抽象为一种数学问题。首先给出如下三个柱子 A、B、C,其中 A 柱子上有从上到下从小叠到大的 N 个云盘。现要求将A柱子上的圆盘都移动到 C 柱子上,其中,每次移动都必须满足:

  1. 每次只能移动一个圆盘
  2. 小圆盘上不能放大圆盘

那么针对这个数学问题,就可以提出相关问题:

  1. 移动 N 个圆盘最少需要多少次
  2. 第 M 步移动的是哪个圆盘以及圆盘移动方向
解题:

设总共有 N 个圆盘,Steps表示总移动次数

1.对于问题1

(1)if N == 1

第1次 1号盘 A—->C 
Steps = 1 次

(2)if N == 2

11号盘 A—->B22号盘 A—->C31号盘 B—->C 
Steps = 3

(3)if N == 3

11号盘 A—->C
​第22号盘 A—->B
​第31号盘 C—->B
​第43号盘 A—->C
​第51号盘 B—->A
​第62号盘 B—->C
​第71号盘 A—->C 
Steps = 7

依次往下推,可以发现这样圆盘个数 N 与移动总次数有这样一个规律

1个圆盘的次数 21次方减1
2个圆盘的次数 22次方减1
3个圆盘的次数 23次方减1...
...
...
​n个圆盘的次数 2的n次方减1

所以能得出一个等式关系 : Steps=2n1 S t e p s = 2 n − 1

2.对于问题2

我们使用 数学归纳法 来理解 Hanoi 递归算法。对于递归其实就是方法内部调用自己,同时也一定有一个结束点。学习过汇编语言的伙伴都应该知道,每次的递归调用实际上行的是一种栈操作,这个递归调用其实是方法调用栈的过程。每次调用时从主线程开始调用方法并进行不断地压栈和出栈操作。其中,方法的调入就是将方法压人栈中,方法的结束就是方法出栈的过程。这样可以保证方法调用的顺序流,即当函数出现多层嵌套时,需要从外到内一层层把函数压入栈中,最后栈顶的函数先执行结束(最内层的函数先执行结束)后出栈,再倒数第二层的函数执行结束出栈,到最后,第一个进栈的函数调用结束后从栈中弹出回到主线程,并且结束。

栈的特点是:FIFO(先进后出)。比如一个方法自己调用自己,其中一个调用过程也就是进栈过程:A->A(1)->A(2)->A(3)

在A(3)时满足某种条件得以退出, 回到 A(2), A(2)结束回到A(1), 再回到A, 出栈过程:A(3)->A(2)->A(1)->A

那么,我们现在来看 Hanoi 的递归问题,首先来看当前 Hanoi 问题的递归算法的实现

### move_hanoi.py
def move(num,go,to):
    global i
    i=i+1
    print('第{}步--移动 {} 号圆盘:'.format(i,num), 'move',go,'to',to)
def hanoi(num,a,b,c):
    if num == 1:
        move(num,a,c)
        return
    hanoi(num-1,a,c,b)
    move(num,a,c)
    hanoi(num-1,b,a,c)

hanoi_move(5,'A','B','C')
i=0

输出效果如下
带你理解 Hanoi 汉诺塔递归算法_第2张图片

看起来是不是很简单?当时一直都不能懂为什么就是这样的递归,实际上再听老师多次提起后,感觉像是懵懵懂懂假装懂了,具体懂了没懂,还需要试试探讨一番。通过证明可能是很好的办法。

为了证明这中递归算法的合理性,可以采用 数学中归纳法 来理解。对于递归算法问题也不要妄想一开始就追踪整个递归的全程调用。在知乎上有很多非常形象的描述。实际上,如果是对于少于三个盘的 Hanoi 移动来说,其实很好理解。但当数目越来越多时,把这个问题展开来想就变得复杂了,这也是为什么我们需要从层与层之间的交接来理解,而不是展开整个递归。

现在讨论,当只需要移动一个圆盘时,此时 N = 1
此时只需移动1次,即将第1个圆盘从A移动到C

步骤  圆盘  柱子移动方向
1     1     A -> C

当移动两个盘子,此时 N = 2

步骤  圆盘  柱子移动方向
1    1     A -> B2    2     A -> C3    1     B -> C

当移动三个圆盘时,此时N = 3

步骤  圆盘  柱子移动方向
1     1     A -> C
2     2     A -> B3     1     C -> B
4     3     A -> C
5     1     B -> A
6     2     B -> C
7     1     A -> C

根据上面的移动情况,我们总结出圆盘移动的规律:

  • 在移动记录中,当盘子的编号和盘子数量相同的时候他们的步骤都是从A移动到C
  • 其他步骤对等

再观察第3个案例中的第 1-3 步 和 第 5-7步

  • 第 1-3 步:目的是从 A 移动到 B。如果我们把 B 当作终点, 那么这里的第 1-3 步理解起来和移动2个圆盘的三个步骤完全相同

    步骤  圆盘  柱子移动方向  B、C 互换
    1       1     A  -> C     (A -> B) 
    2       2     A ->  B     (A -> C) 
    3       1     C ->  B     (B -> C)
    
    即此时相当于 (XCHG B,C),将 B、C 进行了互换
  • 第 5-7 步:目的是从 B 移动到 C。如果我们把 C 当作终点, 那么这里的 5-7 步理解起来和移动2个圆盘的三个步骤完全相同

    步骤  圆盘  柱子移动方向  B、A 互换
    5       1     B  -> A     (A -> B) 
    6       2     B ->  C     (A -> C) 
    7       1     A ->  C     (B -> C)
    
    即此时相当于 (XCHG B,A),将 B、A 进行了互换

    那么,我们将以上得到的规律可以总结为

    • 1.只有一个圆盘,Steps=1, 从 A 移动到 C 即结束
    • 2.当有 N 个盘,Steps=2^n-1 为奇数,中间都是动作都是从 A 移动到 C
    • 3.中间动作之上都可以认为是: 从 A 移动到 B
    • 4.中间动作之下都可以认为是: 从 B 移动到 C

我们用伪代码表示以上规律

Function MOVE(N,A,B,C)
    IF N ==1 THEN
        Print A;"TO",C
    ELSE 
        CALL MOVE(N-1,A,C,B);//MOVE(N,A,B,C)中 B、C 互换,N=N-1
        Print A;"TO",C
        CALL MOVE(N-1,B,A,C);//MOVE(N,A,B,C)中 B、A 互换,N=N-1
    END IF

这就是对 Hanoi 递归算法的归纳过程

资料引用:

  1. 汉若塔互动百科
  2. 函数调用栈 剖析+图解

你可能感兴趣的:(Algorithm)