从汉诺塔问题看递归实质(用递归解决的两个小问题霍纳法则,排列问题)

来源:互联网 发布:红色警戒2网络打不开 编辑:程序博客网 时间:2024/06/05 04:52

汉诺塔问题

汉诺塔问题发源于古老的梵天寺之塔意识。传说I世界诞生的时候,有一座摞了64个黄金碟子的钻石塔(记为A),碟子从小到大的次序自底向上的摞在塔上,除此之外还有两个钻石塔(B和C),从世界诞生之时起,梵天寺的僧侣们就在通过塔C从塔A挪到塔B,因为碟子非常中,每次只能挪一块。另外,任意时候都不能大碟子在小碟子上。根据传说,当僧侣们完成任务的时候就是世界的末日。


首先,我们从简单的例子开始分析,然后再总结出一般规律。
当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柱。

不难给出这样的解法

第一步:将n-1个盘子从A柱移动至B柱(借助C柱为过渡柱)
第二步:将A柱底下最大的盘子移动至C柱
第三步:将B柱的n-1个盘子移至C柱(借助A柱为过渡柱)


C++代码如下:

void TowerOfHanoi(int n,tower x,tower y,tower z){    if(n){        TowerOfHanoi(n-1,x,z,y);        std::cout<<"move top disk from tower "<<char(x)<<" to top of tower "<<char(y)<<std::endl;        TowerOfHanoi(n-1,z,y,x);    }}
这个问题,可能是递归最简单的一个小例子了,那么递归到底是什么呢?

递归的实质
简单的来说,递归就是一个函数直接或间接地调用自身,是为直接或间接递归。一般来说,递归需要有边界条件、递归前进段和递归返回段(其实就是进栈出栈的操作)。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。用递归需要注意以下两点:(1) 递归就是在过程或函数里调用自身。(2) 在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口。

递归 缺点明显,递归解题相对常用的算法如普通循环等,运行效率较低。因此,应该尽量避免使用递归,除非没有更好的算法或者某种特定情况,递归更为适合的时候。在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,因此递归次数过多容易造成栈溢出。

从方法论意义上说,递归方法是一种从简单到复杂、从低级到高级的可连续操作的解决问题的方法。它的每一步骤都是能行可操作的,各步骤之间是连续转换的。递归定义是用简单的、自明的要素描述、构造、说明复杂的整体。递归方法是通过解决简单的问题来解决复杂 的问题。在人们的思维过程中,普遍存在着递归机制。递归方法是一种处理问题的精致技巧、解决问题的有效方法。从哲学方法论角度研究递归方法,具有重要的意义。

递归包含着还原。所谓“还原”就是找到最基本的初始元素和最基本的操作步骤。递归方法与可操作性问题相关。可递归的,就是可还原的,也就是可操作的。某些问题的解决是一个有始有终、环环紧密相联的过程中,要得到最终结果,必须从第一步做起,任何一步都不能缺少;任何相邻的两步,前一步完成之后才能完成后一 步。这样的问题就必须用递归方法来解决。

递归定义具有这样的特点:要证明由归纳定义所确定的某一类对象中的每一个都有某个性质,只需证明满足定义中陈述的规则的对象都有该性质。例如,要证明一个形式系统的每一定理都有性质P,只需要证明满足对定理的定义规则的公式都有性质P。

再从递归的实质回到具体问题中,我们来看这样几个小问题

1.排列问题

给定n个不同的元素,列出所有可能的排列数。(以int数组为例)

void Perm(int *a,int k,int n){    if(k==n){//输出排列        for(int i=0;i<n;i++)            std::cout<<a[i]<<" ";        std::cout<<std::endl;    }    else//a[k:n]中包含多个排列,递归生成这些排列        for(int i=k;i<n;i++){            int t=a[k];            a[k]=a[i];            a[i]=t;            Perm(a,k+1,n);            //a[k+1:n]的所有排列            t=a[k];            a[k]=a[i];            a[i]=t;        }}
2.霍纳法则

假设有n+2个实数a0,a1,…,an,和x的序列,要对多项式Pn(x)= anxn+an-1xn-1+…+a1x+a0求值,直接方法是对每一项分别求值,并把每一项求的值累加起来,这种方法十分低效,它需要进行n+(n-1)+…+1=n(n+1)/2次乘法运算和n次加法运算。有没有更高效的算法呢?答案是肯定的。通过如下变换我们可以得到一种快得多的算法,即Pn(x)= anxn +an-1xn-1+…+a1x+a0=((…(((anx +an-1)x+an-2)x+ an-3)…)x+a1)x+a0,这种求值的安排我们称为霍纳法则。

//以三个项为例
int LEN=3
int hornor(int list[],int n,int x)//利用递归实现霍纳规则{ if(n == LEN-1) { return list[LEN-1];//递归出口 } else { return hornor(list,n+1,x)*x+list[n];//递归过程 }}int main() { int a[3]={1,1,1};//数组表示多项式的系数 int x=3;//多项式的自变量值 int result=0;//存放结果 result = hornor(a,0,3); std::cout<<result<<std::endl; return 0;}





原创粉丝点击