(原创)Hanoi塔问题的递归方法与非递归方法(java实现)

原创 2015年11月18日 00:13:18

本文讨论了Hanoi塔问题的递归方法与非递归方法,给出了java实现的代码,并比较了它们的效率。

法国数学家爱德华·卢卡斯曾编写过一个印度的古老传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片:一次只移动一片,不管在哪根针上,小片必须在大片上面。僧侣们预言,当所有的金片都从梵天穿好的那根针上移到另外一根针上时,世界就将在一声霹雳中消灭,而梵塔、庙宇和众生也都将同归于尽。

文章中我们假设汉诺塔个数为正整数n,三个盘子为A,B,C,其中C是中介盘,我们要遵守移动规则将A上的盘子要全部通过C移动到B。

1.递归方法

如果汉诺塔上盘子个数n=1时显然直接将A上的盘子移动到B即可,当n=2时,方法也很简单,只要将第一块盘子先从A移动到C,再将第二块盘子从A移动到B,再将第一块盘子从C移动到A。实际上,表达的时候不必要强调第几块盘子,而只需要像从A移动到B这样描述,也能清楚的知道意思(因为总是只能移动每个汉诺塔最顶上的盘子)。那么n=2时解决办法的表示就是:A->C,A->B,C->B。下面我们都采用这种简洁明了的表示。
要知道如何将n块盘子从A通过C移动到B,我们可以先将上面的n-1块盘子从A通过B移动到C,再将最大的盘子从A移动到B,这时再将上面的n-1块盘子从C通过A移动到B。这就是递归算法解决Hanoi塔问题的思路。代码如下:

/**
     * 将A汉诺塔上的n个盘子通过C移动到B的递归方法
     * @param n   //汉诺塔上盘子的个数
     * @param A   //开始时有盘子的汉诺塔
     * @param B   //要将盘子移动到上面的目标汉诺塔
     * @param C   //中介汉诺塔
     * @throws IllegalArgumentException when n<=0
     */
    public static void HanoiTowers1(int n,char A,char B,char C){
        if(n<=0){
            throw new IllegalArgumentException("n must be >=1");
        }
        if(n==1){
            System.out.println(A+"->"+B);
        }
        else{
            HanoiTowers1(n-1,A,C,B);     // 将除去最大的盘子的n个盘子从A通过B移动到C
            System.out.println(A+"->"+B);//将最大的盘子从A移动到B
            HanoiTowers1(n-1,C,B,A);     //将除去最大的盘子的n-1个盘子从C通过A移动到B
        }
    }//HanoiTowers1(int n,char A,char B,char C)

    public static void HanoiTowers1(int n){
        HanoiTowers1(n,'A','B','C');
    }//HanoiTowers1(int n)

2.非递归方法

要使用非递归方法,我们必须找到Hanoi塔移动方法的规律。根据递归方法的思想,我们可以求出移动Hanoi塔所需的步骤数为2^n-1。设移动n块盘子需要Hanoi(n)步,则Hanoi(n)=2*Hanoi(n-1)+1,而Hanoi(1)=1,容易求出Hanoi(n)=2^n-1.
我们先来讨论第m步移动的盘子是第几块(盘子按从小到大顺序排序)。首先,容易知道当m=2^(i-1)时,这时一定移动的是第i块盘子。因为要移动A塔上的第i块盘子时,一定得是前面i-1块盘子已经按顺序在另一块盘子上堆好了,而堆好i-1块盘子根据上面所说需要2^(i-1)-1个步骤,故第2^(i-1)第一次移动第i块盘子。而不管要移动的盘子是多少块,将前面盘子摆放好的方法是一样的(这里说的一样是把B和C看作一样的盘子时的一样,不区分中介盘,只考虑移动盘子的次序)比如说n=3和n=4时,前面三步将前两块盘子摆放好的方式是一样的,n=3时前面三步:A->B(第一块盘子),A->C(第二块盘子),B->C(第一块盘子)。n=4时前面三步:A->C(第一块盘子),A->B(第二块盘子),C->B(第一块盘子)。这里可以看出不考虑B,C盘的区别,移动的盘子是一样的。根据这个我们可以将m二进制展开为m=a[i]2^i+…+a[0]*2^0,其中a[i]=0或1,有了这个二进制展开,我们可以知道i从0开始数第一个使a[i]不等于0的i,第i+1块盘子就是第m步要移动的盘子。再一次强调得到这个结论的根据就是移动第N块盘子之前得先让前N-1块盘子保持顺序,而要让N-1块盘子保持顺序得花2^(N-1)-1步。当然这并不是严格的数学证明,但是得到这个结论之后用数学归纳法并不难给出一个完整的证明。我们这里略去严格的证明,只说明结论是:*m用二进制表示的最低位bit为1的位置为p,则第m步移动的是第p块盘子
上面的讨论我们没有区分B盘和C盘,现在我们要来讨论具体的移动方法了,先来看几个例子:
n=2(括号里代表移动的是第几块盘子):
(1)A->C
(2)A->B
(1)C->B
n=3:
(1)A->B
(2)A->C
(1)B->C
(3)A->B
(1)C->A
(2)C->B
(1)A->B

n=4:
(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)的移动情况:n=2时:A->C->B,n=3时:A->B->C->A->B,n=4时:A->C->B->A->C->B。可以发现移动方式其实可以总结为两种1.A->C->B,2.A->B->C接下来就是不停的循环这种移动方式直到停止,可以看到(1)在n为偶数的时候移动方式是A->C->B,在n为奇数时移动方式是A->B->C,如果我们观察(3)会发现移动规律和(1)一样,而(2)正好与它们相反。
这并不是偶然,我们可以总结出规律第奇数块盘子在n为奇数时移动方式为A->B->C,在n为偶数时移动方式为A->C->B,而偶数相反。同样我们这里不会给出严格的数学证明,因为根据前面关于第m步会移动哪一块盘子的讨论当中其实我们就可以发现这个规律,当然我们也可以从之前得讨论之中得到一个严格的数学证明,但我们这里在意的仅仅是结果。更何况,我们可以用实践来检验一下我们的方法。
根据以上讨论的两点结论,我们可以给出非递归算法实现的代码:

/**
     * 将A汉诺塔上的n个盘子通过C移动到B的非递归方法
     * @param n 汉诺塔上盘子个数
     * @throws IllegalArgumentException when n<=0
     */
    public static void HanoiTowers2(int n){
        if(n<=0){
            throw new IllegalArgumentException("n must be >=1");
        }
        char[] hanoiPlate=new char[n];   //记录n个盘子所在的汉诺塔(hanoiPlate[1]='A'意味着第二个盘子现在在A上)
        char[][] next=new char [2][3];   //盘子下次会移动到的盘子的可能性分类
        int index[]=new int[n];

        //根据奇偶性将盘子分为两类
        for(int i=0;i<n;i=i+2){
            index[i]=0;
        }
        for(int i=1;i<n;i=i+2){
            index[i]=1;
        }

        //一开始所有盘子都在A上
        for(int i=0;i<n;i++){
            hanoiPlate[i]='A';
        }

        //n的奇偶性对移动方式的影响
        if(n%2==0){
            next[0][0]='C';
            next[0][1]='A';
            next[0][2]='B';
            next[1][0]='B';
            next[1][1]='C';
            next[1][2]='A';
        }
        else
        {
            next[1][0]='C';
            next[1][1]='A';
            next[1][2]='B';
            next[0][0]='B';
            next[0][1]='C';
            next[0][2]='A';
        }

        //开始移动
        for(int i=1;i<(1<<n);i++){                  //总共要执行2^n-1(1<<n-1)步移动
            int m=0;                                //m代表第m块盘子hanoiPlate[m]

            //根据步骤数i来判断移动哪块盘子以及如何移动
            for(int j=i;j>0;j=j/2){
                if(j%2!=0){
                    System.out.println("("+(m+1)+")"+hanoiPlate[m]+"->"+next[index[m]][hanoiPlate[m]-'A']);
                    hanoiPlate[m]=next[index[m]][hanoiPlate[m]-'A'];
                    break;                           //移动盘子后则退出这层循环
                }
                m++;
            }
        }
    }

3.比较

计算的步骤数都是非递归算法要更多,这是因为非递归算法有两个for循环的嵌套,递归算法占用内存更多,我们直接通过实验来对比它们在运算时间上的差异。
实验结果:

n=10时: 递归方法耗时: 0 非递归方法耗时: 0
n=15时: 递归方法耗时: 0 非递归方法耗时: 0
n=20时: 递归方法耗时: 4 非递归方法耗时: 26
n=25时: 递归方法耗时: 154 非递归方法耗时: 861
n=30时: 递归方法耗时: 5059 非递归方法耗时: 29695

通过结果我们可以看到递归算法的时间效率远高于非递归算法。非递归算法耗用了太多时间在for循环,我们可以试着改进一下非递归算法。
将非递归算法中的for循环模块修改为:

for(int i=1;i<(1<<n);i++){                  //总共要执行2^n-1(1<<n-1)步移动
            int m=0;                                //m代表第m块盘子hanoiPlate[m]
            int j=i;

            while((j<<31)>>31==0&&j>0){
                j=j>>1;
                m++;
            }
    }

再次进行对比实验,结果如下:
n=10时: 递归方法耗时: 0 非递归方法耗时: 0
n=15时: 递归方法耗时: 0 非递归方法耗时: 0
n=20时: 递归方法耗时: 4 非递归方法耗时: 8
n=25时: 递归方法耗时: 156 非递归方法耗时: 267
n=30时: 递归方法耗时: 5408 非递归方法耗时: 8596

相比修改之前速度快了不少,但是仍然比递归算法要慢。

4.总结

虽然根据比较的结果,我们可以发现递归算法可能略优于非递归算法,不过非递归算法在回答Hanoi塔问题第m步做什么的时候会比递归算法快,因为非递归算法可以直接求第m步要移动哪一块,从哪里移动到哪里(上文没有给出解释,见附注),而递归算法要一步步模拟到最后才能知道结果,很可能由于n过大,而无法的到结果。

附注
设m=2^k[1]+2^k[2]+2^k[3]+2^k[s ] , 这里k[s]>k[s-1]>…>k[1]。
那么第m步要移动的就是第k[1]+1块盘子,我们知道k[1]+1块盘子的移动规律,它的移动周期是3,且是送A开始移动的,我们只要知道它是第几次被移动就可以了。它是第2^[(k[2]+k[3]+…+k[s])-(s-1)(k[1]+1)]+1次。可以通过证明第2^i次移动,第i+1块盘子移动1次。第2^(i+p)次,第i+1块盘子移动2^(p-1)次,其中p>=1来得出上面的结论。

版权声明:本文为博主原创文章,未经博主允许不得转载。

经典递归解决汉诺塔!

算法:当只有一个盘子的时候,只需要从将A塔上的一个盘子移到C塔上。             当A塔上有两个盘子是,先将A塔上的1号盘子(编号从上到下)移动到B塔上,再将A塔上的2号盘子移动的C塔上,...

hanoi塔问题解析(一) c++实现

什么是hanoi塔? 汉诺塔问题:古代有一个梵塔,塔内有三个座A、B、C,A座上有64个盘子,盘子大小不等,大的在下,小的在上。有一个和尚想把这64个盘子从A座移到B座,但每次只能允许移动一个盘子...

Hanoi塔问题(递归解决)

首先了解一下递归的思想:         设计一个递归函数,一般需要首先分析以下问题。 * 退出递归的边界条件及其边界值。即分析在什么情况下,才可以直接求出问题的解,从而退出递归。 * 执行递归的通式...

汉诺塔递归及非递归解法

1. 经典递归解法 #include void mov(char a, char b) { std::cout "

汉诺塔递归分析和非递归算法

一位法国数学家曾编写过一个印度的古老传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这...

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

(Hanoi)汉诺塔java实现程序

汉诺塔:汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序...

Java Hanoi塔问题简单分治算法实现

Java Hanoi塔问题简单分治算法实现问题描述输入:圆盘数n, 3根系杆——起始杆A、过渡杆B、目标杆C 输出:从起始杆到目标杆过程的最少步骤算法思想分治算法思想 如果A上只有一个盘,直接移动...

汉诺塔问题的递归和非递归算法

汉诺塔问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上...

汉诺塔非递归算法分析与实现

汉诺塔的递归算法很容易理解,也非常容易实现。下面,本文讨论了汉诺塔问题的非递归算法,核心内容就是栈的使用技巧。 首先,对于每个柱子来说,就是一个栈,这个栈有个特点就是,大数放在下面,小数放在上面。在...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:(原创)Hanoi塔问题的递归方法与非递归方法(java实现)
举报原因:
原因补充:

(最多只允许输入30个字)