历史传说:
在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片:一次只移动一片,不管在哪根针上,小片必须在大片上面。僧侣们预言,当所有的金片都从梵天穿好的那根针上移到另外一根针上时,世界就将在一声霹雳中消灭,而梵塔、庙宇和众生也都将同归于尽。
不管这个传说的可信度有多大,如果考虑一下把64片金片,由一根针上移到另一根针上,并且始终保持上小下大的顺序。这需要多少次移动呢?这里需要递归的方法。假设有n片,移动次数是f(n).显然f⑴=1,f⑵=3,f⑶=7,且f(k+1)=2*f(k)+1。此后不难证明f(n)=2^n-1。n=64时,f(64)= 2^64-1=18446744073709551615。假如每秒钟一次,共需多长时间呢?一个平年365天有 31536000 秒,闰年366天有31622400秒,平均每年31556952秒,计算一下,18446744073709551615/31556952=584554049253.855年
这表明移完这些金片需要5845亿年以上,而地球存在至今不过45亿年,太阳系的预期寿命据说也就是数百亿年。真的过了5845亿年,不说太阳系和银河系,至少地球上的一切生命,连同梵塔、庙宇等,都早已经灰飞烟灭。
***********************************************************************************************************************************************************************************
汉诺塔问题的限制条件:
1.在小圆盘上不能放大圆盘。
2.在三根柱子之间一回只能移动一个圆盘。
3.只能移动在最顶端的圆盘。
首先,我们从简单的例子开始分析,然后再总结出一般规律。
当n = 1的时候,即此时只有一个盘子,那么直接将其移动至C即可。移动过程就是 A -> C
当n = 2的时候,这时候有两个盘子,那么在一开始移动的时候,我们需要借助B柱作为过渡的柱子,即将A柱最上面的那个小圆盘移至B柱,然后将A柱底下的圆盘移至C柱,最后将B柱的圆盘移至C柱即可。那么完整移动过程就是A -> B , A -> C , B -> C
当n = 3的时候,那么此时从上到下依次摆放着从小到大的三个圆盘,根据题目的限制条件:在小圆盘上不能放大圆盘,而且把圆盘从A柱移至C柱后,C柱圆盘的摆放情况和刚开始A柱的是一模一样的。所以呢,我们每次移至C柱的圆盘(移至C柱后不再移到其他柱子上去),必须是从大到小的,即一开始的时候,我们应该想办法把最大的圆盘移至C柱,然后再想办法将第二大的圆盘移至C柱......然后重复这样的过程,直到所有的圆盘都按照原来A柱摆放的样子移动到了C柱。
那么根据这样的思路,问题就来了:
如何才能够将最大的盘子移至C柱呢?
那么我们从问题入手,要将最大的盘子移至C柱,那么必然要先搬掉A柱上面的n-1个盘子,而C柱一开始的时候是作为目标柱的,所以我们可以用B柱作为"暂存"这n-1个盘子的过渡柱,当把这n-1的盘子移至B柱后,我们就可以把A柱最底下的盘子移至C柱了。
而接下来的问题是什么呢?
我们来看看现在各个柱子上盘子的情况,A柱上无盘子,而B柱从上到下依次摆放着从小到大的n-1个盘子,C柱上摆放着最大的那个盘子。
所以接下来的问题就显而易见了,那就是要把B柱这剩下的n-1个盘子移至C柱,而B柱作为过渡柱,那么我们需要借助A柱,将A柱作为新的"过渡"柱,将这n-1个盘子移至C柱。
*********************************************************************************************************************************************************************
作者:Adherer
来源:CSDN
原文:https://blog.csdn.net/liujian20150808/article/details/50793101
版权声明:本文为博主原创文章,转载请附上博文链接!
*********************************************************************************************************************************************************************
该问题可以分解成以下子问题:
第一步:将n-1个盘子从A柱移动至B柱(借助C柱为过渡柱)
第二步:将A柱底下最大的盘子移动至C柱
第三步:将B柱的n-1个盘子移至C柱(借助A柱为过渡柱)
解:(1)n == 1
第1次 1号盘 A---->C sum = 1 次
(2) n == 2
第1次 1号盘 A---->B
第2次 2号盘 A---->C
第3次 1号盘 B---->C sum = 3 次
(3)n == 3
第1次 1号盘 A---->C
第2次 2号盘 A---->B
第3次 1号盘 C---->B
第4次 3号盘 A---->C
第5次 1号盘 B---->A
第6次 2号盘 B---->C
第7次 1号盘 A---->C sum = 7 次
不难发现规律:1个圆盘的次数 2的1次方减1
2个圆盘的次数 2的2次方减1
3个圆盘的次数 2的3次方减1
。 。 。 。 。
n个圆盘的次数 2的n次方减1
故:移动次数为:2^n - 1
n的阶乘问题
再说一个例子:计算n的阶乘
f(n) = n!
其递归算法如下:
int factorial(int n){ if(n == 1) return 1; else return n * factorial(n-1); }
这段程序加载到内存的分配图如下:
(图片来源于“码农翻身”公众号)
由于递归是函数自身调用自身,所以程序被编译后代码段中只有一份代码。
递归调用是如何进行的呢?
注意看堆栈中的栈帧啊, 每个栈帧就代表了被调用中的一个函数, 这些函数栈帧以先进后出的方式排列起来,就形成了一个栈, 栈帧的结构如下图所示:
(图片来源于“码农翻身”公众号)
相信大家还记得《数据结构》(严蔚敏版)一书中提到的“工作记录”就是指函数栈帧。栈顶指针被称为“当前环境指针”。
忽略到其他内容, 只关注输入参数和返回值的话,阶乘函数factorial(4)的工作栈如下图所示:
(图片来源于“码农翻身”公众号)
其计算过程如下图所示:
(图片来源于“码农翻身”公众号)
注意, 每个递归函数必须得有个终止条件, 要不然就会发生无限递归了, 永远都出不来了。
当然针对于此递归算法,对于n的值是有限制的。因为堆栈容量是有限的,如果n值太大程序会崩掉。
该如何解决呢?
从上面的代码中可以知道“factorial(n) = n * factorial(n-1 ) ” ,这个计算式是整个程序的核心。 图中每个栈帧都需要记录下当前的n的值, 还要记录下一个函数栈帧的返回值, 然后才能运算出当前栈帧的结果。 也就是说使用多个栈帧是不可避免的。
可以使用下面的递归算法:
int factorial(int n,int result){ if(n == 1){ return result; } else{ return factorial(n-1,n * result); } }
注意函数的最后一个语句, 就不是 n * factorial(n-1) 了, 而是直接调用factorial(....) 这个函数本身, 这就带来了巨大的好处。
计算过程如下:
当执行到factorial(1, 24)的时候直接就可以返回结果了。
这就是妙处所在了,计算机发现这种情况,只用一个栈帧就可以搞定这些计算,无论n有多大。
(图片来源于“码农翻身”公众号)
这就是所谓的“尾递归”了, 当递归调用是函数体中最后执行的语句并且它的返回值不属于表达式一部分时, 这个递归就是尾递归。
现代的编译器就会发现这个特点, 生成优化的代码, 复用栈帧。 第一个算法中因为有个n * factorial(n-1) , 虽然也是递归,但是递归的结果处于一个表达式中,还要做计算, 所以就没法复用栈帧了,只能一层一层的调用下去。