</div>
<!--一个博主专栏付费入口-->
<!--一个博主专栏付费入口结束-->
<link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/ck_htmledit_views-4a3473df85.css">
<div id="content_views" class="markdown_views">
<!-- flowchart 箭头图标 勿删 -->
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<path stroke-linecap="round" d="M5,0 0,2.5 5,5z" id="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></path>
</svg>
<h1 id="前言"><a name="t0"></a>前言</h1>
先来看两个有趣的故事
- 从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,故事讲的是从前有座山,山上有座庙,庙里有个老和尚,老和尚在给小和尚讲故事,故事讲的是从前有座山,山上有座庙。。。
-
- 问:GNU全称是什么?
- 答:GNU is Not UNIX
- 问:第一个单词GNU的全称是什么?
- 答:GNU is Not UNIX
- 问:第一个单词GNU的全称是什么?
- 答:GNU is Not UNIX
- 。。。
类似于上述这种使用自己来定义自己的结构,称之为递归。
递归思想很有意思,在很多方面都有实际的用途。在计算机编程中,对于一些特定的问题,常规方法往往会显得很繁琐,抓不住问题的本质,这种时候采用递归的思想编程,往往能够写出很优雅的代码。
汉诺塔问题
“汉诺塔”是由数学家爱德华 · 卢卡斯于1883年发明的游戏,爱德华当初用了一个神话来描述这个问题
传说越南河内某间寺院有三根银棒,上串 64 个金盘。寺院里的僧侣依照一个古老的预言,以上述规则移动这些盘子;预言说当这些盘子移动完毕,世界就会灭亡。这个传说叫做梵天寺之塔问题(Tower of Brahma puzzle)。
—— 维基百科
这个问题被数学抽象描述就是
- 问题:有3根细柱子(A、B、C),其中A柱子上有64个圆盘,64个圆盘按照从上到下由小到大的次序排列。求一共需要移动多少次才能将A柱子上的64个圆盘全部移动到B柱子上?
- 移动规则(移动前、移动中、移动后,都必须遵循)
- 一次只能移动柱子最上端的一个圆盘
- 小圆盘上面不能放大圆盘
对于64个圆盘问题,太复杂太庞大了。可以先从最小的情况开始考虑
一层汉诺塔
算法图解如下
规律:
- 将1个圆盘,从A柱子移动到B柱子
记作:A→BA→B
两层汉诺塔
算法图解如下
规律:
- 将2-1个圆盘,从A柱子移动到C柱子
- 将第2个圆盘,从A柱子移动到B柱子
- 将2-1个圆盘,从C柱子移动到B柱子
记作:
- A→CA→C
三层汉诺塔
算法图解如下
规律
- 将3-1个圆盘,从A柱子移动到C柱子(第1次~第3次)
- 将第3个圆盘,从A柱子移动到B柱子(第4次)
- 将3-1个圆盘,从C柱子移动到B柱子(第5次~第7次)
记作:
- A→B,A→C,B→CA→B,A→C,B→C
规律一
由于第四层汉诺塔整个算法图解过程已经很长了,所以这里就不列举了。不过,从前面三层汉诺塔图解算法中,我们足以发现了n层汉诺塔移动的一般性规律
- 将n-1个圆盘,从A柱子移动到C柱子
- 将第n个圆盘,从A柱子移动到B柱子
- 将n-1个圆盘,从C柱子移动到B柱子
我们将上述三个步骤,调换一下顺序
- 将n-1个圆盘,从A柱子移动到C柱子
- 将n-1个圆盘,从C柱子移动到B柱子
- 将第n个圆盘,从A柱子移动到B柱子
调换顺序之后的发现,其实第一步和第二步合起来,便是将n-1个圆盘从A柱子移动到B柱子上 。
因此,算法可以进一步简化为
- 将n-1个圆盘,从A柱子移动到B柱子
- 将第n个圆盘,从A柱子移动到B柱子
将n-1个圆盘从A移动到B的过程,就是将n-2个圆盘从A移动到B,与将第n-1个圆盘从A移动到B;将n-2个圆盘从A移动到B的过程,就是将n-3个圆盘从A移动到B,与将第n-2个圆盘从A移动到B;……;将1个圆盘从A移动到B的过程,就是将第1-1个圆盘从A移动到B,与将第1个圆盘从A移动到B。
很明显,这样的一个过程,就是递归。在整个递归过程中,无论从第几层开始,最后会终止于一个确定的步骤——将1个圆盘从A移动到B,而每一层最关键的一个确定步骤就是——将第n个圆盘从A移动到B。因此整个递归过程其实都是在做一件事情——将1个圆盘从A移动到B !
规律二
由规律一,我们可以得知,整个递归过程中都是在重复做一件事情——将1个圆盘从A移动到B。但事实上,一旦圆盘数目超过1个时,就需要一个借助3根柱子中的一根作为中转站,通过折中的方法实现将圆盘从A的移动到B。
- 将n-1个圆盘,从A柱子移动到C柱子
- 将第n个圆盘,从A柱子移动到B柱子
- 将n-1个圆盘,从C柱子移动到B柱子
由上述总结规律可知,这样的一个过程,起到中转站的作用的柱子似乎就是C柱子。
但是事实上绝不是这样,很多情况下,起到中转站作用的柱子不是绝对的!
分析一个最具代表的模型,三层汉诺塔的表达式
- A→B,A→C,B→CA→B,A→C,B→C
从整个算法过程来看,以B为中转站能够将2个盘子从A移动到C,以A为中转站能够将2个盘子从C移动到B。在更多层的汉诺塔算法过程中,往往是A、B、C都会对某一段过程起到中转站的作用,因此使用中转站的概念来抽象出基本表达式是难以把握的。
因此,这里建议采用了一种目标柱子的概念(没听过对吧,我自创的)来理解上述的过程。
- 首先,将算法中最特殊的一个步骤——从A移动到B,看做本次移动的目标柱子是B,因此在这样一个步骤中,→→ 右边出现最多的就是B。
OK!到此,我们可以得知,在整个算法中,只有两个目标柱子——B和C存在。
算法实现
通过上面的一系列分析,我们得出了两个最为关键性的结论。根据这两个相关性结论,编写代码实现算法
C语言实现算法
#include <stdio.h>
//在参数x、y、z中,永远以中间那个参数为“目标柱子”
void hanoi(int n, char x, char y, char z)
{
if (n == 1) {
//递归的最后一种情况,1层汉诺塔:从A移动到B
printf("第%d层圆盘移动方向 : %c -> %c\n", n, x, y);
} else {
//第一步,以C柱子为目标(规律二)
hanoi(n - 1, x, z, y);
//第二步,以B柱子为目标,直接输出(规律一)
printf("第%d层圆盘移动方向 : %c -> %c\n", n, x, y);
//第三步,以B柱子为目标(规律二)
hanoi(n - 1, z, y, x);
}
}
int main(void)
{
hanoi(1, 'A', 'B', 'C');
putchar('\n');
hanoi(2, 'A', 'B', 'C');
putchar('\n');
hanoi(3, 'A', 'B', 'C');
putchar('\n');
hanoi(4, 'A', 'B', 'C');
return 0;
}
/* 运行结果:
第1层圆盘移动 : A -> B
第1层圆盘移动 : A -> C
第2层圆盘移动 : A -> B
第1层圆盘移动 : C -> B
第1层圆盘移动 : A -> B
第2层圆盘移动 : A -> C
第1层圆盘移动 : B -> C
第3层圆盘移动 : A -> B
第1层圆盘移动 : C -> A
第2层圆盘移动 : C -> B
第1层圆盘移动 : A -> B
第1层圆盘移动 : A -> C
第2层圆盘移动 : A -> B
第1层圆盘移动 : C -> B
第3层圆盘移动 : A -> C
第1层圆盘移动 : B -> A
第2层圆盘移动 : B -> C
第1层圆盘移动 : A -> C
第4层圆盘移动 : A -> B
第1层圆盘移动 : C -> B
第2层圆盘移动 : C -> A
第1层圆盘移动 : B -> A
第3层圆盘移动 : C -> B
第1层圆盘移动 : A -> C
第2层圆盘移动 : A -> B
第1层圆盘移动 : C -> B
*/
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
如分析中那样,递归算法采用的终止条件是——1层汉诺塔情况,但实际上这个算法是缺乏健壮性不合理的,而且存在冗余。因为它没能考虑汉诺塔最特殊的情况——0层汉诺塔 。当程序运行hanoi(0)
时,函数无法返回,造成异常错误。事实上,0层汉诺塔是存在且合理的,而且把0层汉诺塔考虑进去才符合我们对汉诺塔问题的本质分析——每一次都是从A移动到B ,而且我们可以发现上述代码中1层汉诺塔也并没有什么特殊之处,函数中两个printf()
语句执行的操作是一模一样的。下面给出改进版本的代码
Python实现算法
def hanoi(n, x, y, z):
if not n:
pass #0层汉诺塔不需要做任何移动
else:
hanoi(n-1, x, z, y)
print("第%d层圆盘移动方向:%c -> %c" % (n, x, y))
hanoi(n-1, z, y, x)
hanoi(0, 'A', 'B', 'C')
# 什么也不输出
hanoi(1, 'A', 'B', 'C')
第1层圆盘移动方向:A -> B
hanoi(2, 'A', 'B', 'C')
第1层圆盘移动方向:A -> C
第2层圆盘移动方向:A -> B
第1层圆盘移动方向:C -> B
hanoi(3, 'A', 'B', 'C')
第1层圆盘移动方向:A -> B
第2层圆盘移动方向:A -> C
第1层圆盘移动方向:B -> C
第3层圆盘移动方向:A -> B
第1层圆盘移动方向:C -> A
第2层圆盘移动方向:C -> B
第1层圆盘移动方向:A -> B
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
改进版本的Python代码,不但能正确计算出汉诺塔问题,而且算法还能够兼容最特殊的情况——0层汉诺塔问题。这才是该问题最优雅的算法。
问题答案
回到汉诺塔的问题上,64个圆盘的汉诺塔需要移动多少次才能完成从A柱子到B柱子。
通过上面发现的规律,以及算法的实现,我们可以得出该算法的递归公式
H(n)=H(n−1)+1+H(n−1)H(n)=H(n−1)+1+H(n−1)
使用递推公式计算汉诺塔移动次数
H(0)=0H(0)=0
H(1)=H(0)+1+H(0)=0+1+0=1H(1)=H(0)+1+H(0)=0+1+0=1
H(2)=H(1)+1+H(1)=1+1+1=3H(2)=H(1)+1+H(1)=1+1+1=3
H(3)=H(2)+1+H(2)=3+1+3=7H(3)=H(2)+1+H(2)=3+1+3=7
H(4)=H(3)+1+H(3)=7+1+7=15H(4)=H(3)+1+H(3)=7+1+7=15
H(5)=H(4)+1+H(4)=15+1+15=31H(5)=H(4)+1+H(4)=15+1+15=31
H(6)=H(5)+1+H(5)=31+1+31=63H(6)=H(5)+1+H(5)=31+1+31=63
......
观察函数与函数值的规律可得知
H(0)=0=20−1H(0)=0=20−1
H(1)=1=21−1H(1)=1=21−1
H(2)=3=22−1H(2)=3=22−1
H(3)=7=23−1H(3)=7=23−1
H(4)=15=24−1H(4)=15=24−1
H(5)=31=25−1H(5)=31=25−1
H(6)=63=26−1H(6)=63=26−1
......
因此,通项表达式为
H(n)=2n−1H(n)=2n−1
当 n = 64
时,H(64) = 18446744073709551615。就算是每秒可以移动一次,也需要5845亿年多,而目前所知的宇宙的年龄也不过才137亿年,所以说就算传说当真,那么距离世界末日还早着呢。。。
总结
递归算法的关键就在于抓住递归中重复的步骤,然后每次递归都是在重复那一关键步骤。递归算法虽然内存开销比较大,但是代码可读性很好,而且体现出了一种数学的优雅美(反正我觉得很美!)。
数学中分形,具有递归美,下面放出福利图片,送给大家欣赏(图片来源:http://www.matrix67.com,侵权必删!)