算法笔记_013:汉诺塔问题(Java递归法和非递归法)

时间:2021-11-05 20:19:02

目录

1 问题描述

2 解决方案 

2.1 递归法

2.2 非递归法


1 问题描述

Simulate the movement of the Towers of Hanoi Puzzle; Bonus is possible for using animation.

e.g. if n = 2 ; A→B ; A→C ; B→C;

if n = 3; A→C ; A→B ; C→B ; A→C ; B→A ; B→C ; A→C;

翻译:模拟汉诺塔问题的移动规则;获得奖励的移动方法还是有可能的。

相关经典题目延伸:

引用自百度百科:

有三根相邻的柱子,标号为A,B,C,A柱子上从下到上按金字塔状叠放着n个不同大小的圆盘,要把所有盘子一个一个移动到柱子C上,并且每次移动同一根柱子上都不能出现大盘子在小盘子上方,请问至少需要多少次移动,设移动次数为H(n)。

首先我们肯定是把上面n-1个盘子移动到柱子B上,然后把最大的一块放在C上,最后把B上的所有盘子移动到C上,由此我们得出表达式:

H⑴ = 1          A—>C

H(2) = 3         A—>B;A—>C;B—>C

H(3) = 7         ...

H(4) = 15

... ...

H(n) = 2*H(n-1)+1 (n>1)

那么我们很快就能得到H(n)的一般式:

H(n) = 2^n - 1 (n>0)


2 解决方案

2.1 递归法

 import java.util.Scanner;

 public class Hanoi {

     //使用递归法求解含有n个不同大小盘子的汉诺塔移动路径,参数n为盘子数,把A塔上盘子全部移动到C塔上,B为过渡塔
public static void recursionHanoi(int n,char A,char B,char C){
if(n == 1){
System.out.print(A+"——>"+C+"\n");
}
else{
recursionHanoi(n-1,A,C,B); //使用递归先把A塔最上面的n-1个盘子移动到B塔上,C为过渡塔
System.out.print(A+"——>"+C+"\n"); //把A塔中底下最大的圆盘,移动到C塔上
recursionHanoi(n-1,B,A,C); //使用递归把B塔上n-1个盘子移动到C塔上,A为过渡塔
}
} public static void main(String[] args){
System.out.println("请输入盘子总数n:");
Scanner in = new Scanner(System.in);
int n = in.nextInt();
recursionHanoi(n,'A','B','C');
}
}

运行结果:

请输入盘子总数n:
2
A——>B
A——>C
B——>C
请输入盘子总数n:
3
A——>C
A——>B
C——>B
A——>C
B——>A
B——>C
A——>C

2.2 非递归法

要使用非递归方法,首先要解决的核心问题就是找到汉诺塔的移动规律。现在问题是把n个盘子从A塔全部移动到C塔,那么先看看n = 2,3,4时,其具体盘子的移动路径结果(PS:移动路径前标号是指A塔上原有的第几个盘子,如(1)代表A塔上原有最上面的那个盘子,依次类推...):

n = 2:

1)A—>B

2)A—>C

1)B—>C

n = 3:

1)A——>C

(2)A——>B

1)C——>B

3)A——>C

1)B——>A

(2)B——>C

1)A——>C

n = 4:

1)A——>B

2)A——>C

1)B——>C

(3)A——>B

1)C——>A

2)C——>B

1)A——>B

4)A——>C

1)B——>C

2)B——>A

1)C——>A

(3)B——>C

1)A——>B

2)A——>C

1)B——>C

从上我们可以发现,n为偶数2,4时路径前标号为(1)的盘子移动路径依次为A——>B——>C,A——>B——>C——>A——>B——>C。n为偶数2,4时路径前标号为(2)的盘子移动路径依次为A—>C,A—>C——>B——>A—>C。而且发现n = 4其标号为(1)和标号为(3)的移动路径一模一样。n为奇数3时路径前标号为(1)和(2)的盘子移动路径依次为A——>C——>B——>A——>C,A——>B——>C。

看到这里,我们可以大胆猜测盘子的具体移动路径与盘子的总个数的奇偶性以及盘子标号的奇偶性有关,而且移动的路径是固定又循环的。

那么现在设定一个二维数组用来存放盘子下次移动的塔:

char next = new char[2][3];

二维数组中行char[0]代表数组下标为偶数的盘子下次要移动的塔

二维数组中行char[1]代表数组下标为奇数的盘子下次要移动的塔

二维数组重列char[0][0]代表盘子现在在A塔准备进行下次移动

二维数组重列char[0][1]代表盘子现在在B塔准备进行下次移动

二维数组重列char[0][2]代表盘子现在在C塔准备进行下次移动

那么下面我们就来根据盘子现在所在塔,设定其下次移动的目的塔(PS:设共有n的盘子):

if(n为偶数)

{

   //数组下标为偶数的盘子移动目的塔,注意上面示例的标号为(1),其数组下标为0

   next[0][0] = ‘B’;   //看n = 4的移动路径中(1)A——>B

   next[0][1] = ‘C’;   //看n = 4的移动路径中(1)B——>C

   next[0][2] = ‘A’;   //看n = 4的移动路径中(1)C——>A

   //数组下标为奇数的盘子移动目的塔

   next[1][0] = ‘C’;   //看n = 4的移动路径中(2)A——>C

   next[1][1] = ‘A’;   //看n = 4的移动路径中(2)B——>A

   next[1][0] = ‘B’;   //看n = 4的移动路径中(2)C——>B

}

If(n为奇数)

{

   //数组下标为偶数的盘子移动目的塔,注意上面示例的标号为(1),其数组下标为0

   Next[0][0] = ‘C’;   //看n = 3的移动路径中(1)A——>C

   Next[0][1] = ‘A’;   //看n = 3的移动路径中(1)B——>A

   Next[0][2] = ‘B’;   //看n = 3的移动路径中(1)C——>B

   //数组下标为奇数的盘子移动目的塔

   Next[1][0] = ‘B’;   //看n = 3的移动路径中(2)A——>B

   Next[1][1] = ‘C’;   //看n = 3的移动路径中(2)B——>C

   Next[1][2] = ‘A’;   //此处根据观察规律假设的

}

到这里,距离使用非递归法解决汉诺塔问题已经有头绪了,此处还有注意一点就是H(n) = 2^n - 1 (n>0),即移动n个盘子需要总次数为2^n - 1 ,即使用非递归法是需要进行循环2^n - 1 次。

 package com.liuzhen.ex2;

 import java.util.Scanner;

 public class Hanoi {

     //使用递归法求解含有n个不同大小盘子的汉诺塔移动路径,参数n为盘子数,把A塔上盘子全部移动到C塔上,B为过渡塔
public static void recursionHanoi(int n,char A,char B,char C){
if(n == 1){
System.out.print(A+"——>"+C+"\n");
}
else{
recursionHanoi(n-1,A,C,B); //使用递归先把A塔最上面的n-1个盘子移动到B塔上,C为过渡塔
System.out.print(A+"——>"+C+"\n"); //把A塔中底下最大的圆盘,移动到C塔上
recursionHanoi(n-1,B,A,C); //使用递归把B塔上n-1个盘子移动到C塔上,A为过渡塔
}
} public static void noRecursionHanoi(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){
//数组下标为偶数的盘子移动目的塔,注意上面示例的标号为(1),其数组下标为0
next[0][0]='B';
next[0][1]='C';
next[0][2]='A';
//数组下标为奇数的盘子移动目的塔
next[1][0]='C';
next[1][1]='A';
next[1][2]='B';
}
else
{
//数组下标为偶数的盘子移动目的塔,注意上面示例的标号为(1),其数组下标为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';
} //开始移动
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){ //此步骤光看代码代码有点抽象,建议手动写一下n = 2时的具体移动路径的j、m值变化
System.out.println("("+(m+1)+")"+hanoiPlate[m]+"->"+next[index[m]][hanoiPlate[m]-'A']);
hanoiPlate[m]=next[index[m]][hanoiPlate[m]-'A'];
break; //移动盘子后则退出这层循环
}
m++;
}
}
} public static void main(String[] args){
System.out.println("请输入盘子总数n:");
Scanner in = new Scanner(System.in);
int n = in.nextInt();
recursionHanoi(n,'A','B','C');
System.out.println("非递归法结果:");
noRecursionHanoi(n);
System.out.println();
}
}

运行结果:

请输入盘子总数n:
2
A——>B
A——>C
B——>C
非递归法结果:
(1)A->B
(2)A->C
(1)B->C 请输入盘子总数n:
3
A——>C
A——>B
C——>B
A——>C
B——>A
B——>C
A——>C
非递归法结果:
(1)A->C
(2)A->B
(1)C->B
(3)A->C
(1)B->A
(2)B->C
(1)A->C

 参考资料:

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