递归——汉诺塔问题(结合代码理解,终于懂了)

问题

汉诺塔问题是一个经典的递归问题,汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子, 在一根柱子上从下往上按照大小顺序摞着 64 片圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一 根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。问要怎么移动圆盘?

image-20230303200815111

​ 图1 汉诺塔

递归思想

​开始说汉诺塔问题之前,我们先来回顾一下递归的主要思想。

​递归的关键思想有两个:

  1. 递归找到边界条件(结束条件),一般作为if语句中的判断条件。
  2. 递归最后一层和其前一层或者是和其他层的关系(即递归的规律)用什么样的关系式来表达,一般作为else语句中的方法体(执行语句)。

​这样说可能听着有点晕,举个例子,如递归中的阶乘问题:

//factorial阶乘
public int factorial(int n){
  if(n == 1){
    return1;
  }else{
    return factorial(n - 1) * n;
  }
}    

这里的n == 1就是阶乘递归的边界条件(结束条件),如果没有结束条件,那么程序就会陷入无限递归过程中,直到栈溢出。递归的过程和二叉树这种数据结构有点类似,可以通过二叉树来理解递归如何将一个复杂的问题分解成若干个简单的子问题。递归实际上是将一个复杂的原问题(二叉树的根节点)一步步分解成一个个简单容易求解的子问题(二叉树的子孙节点),然后到达边界条件(结束条件),最后将求解的一个个子问题通过递归回溯联系起来,一层一层地回到二叉树的根节点,最终求解出原问题。在递归的过程中要注意,不要试图去跟踪复杂的递归过程,只要找出递归的规律即可,即递归的关系表达式,也就是说要完成最后一步,那在完成最后一步的前一步要做什么,要懂得化繁为简,例如我们在求funtion(n)的时候,就假设funtion(n -1)的结果已经求出来了(通过递归回溯求出来的),这样问题简单得多,就好解决多了。

尝试

​在正式讲解汉诺塔问题之前,最好自己去玩一玩,看能不能找出其中的规律,如果五个圆盘玩不了,那就那叠三个圆盘来玩一玩,总之,一定要去玩一玩,并试着去找出规律,才会理解下面是在讲什么。

定义

  1. 在该汉诺塔问题中,假设是从A柱上将所有的圆盘移动到C柱上。
  2. 图1中五个圆盘从上到下分别用1,2,3,4,5来表示,如1圆盘,2圆盘。
  3. 开始柱是指开始移动前存放n个圆盘的柱子,中转柱是指中间状态存放(n - 1)个盘子的柱子,目标柱是指n个盘子最终要移动到的柱子。以图1中五层汉诺塔为例,要将A柱(塔)上的五个圆盘移动到C柱(塔)上,则A柱为开始柱,有五个圆盘;B柱为中转柱,在中间状态的时候存放了1-4圆盘(共四个圆盘);C柱为目标柱,是五个圆盘最终存放的柱子。值得注意的是,这里的开始柱、中转柱和目标柱并不是固定一成不变的,而是会随着圆盘的移动而动态变化的。如果这里听着有点迷糊,那就接着往下看,回过头来就会恍然大悟了。

汉诺塔(递归)规律

  1. 可以很容易找出递归的结束条件,假设A柱上只有一个圆盘,那么直接从A柱上移动一个圆盘到C柱上,即结束条件为当n==1时,从A柱上移动一个圆盘到C柱上。
  2. 我们需要将要移动的圆盘分为两部分,其中将最大的一个圆盘作为一部分,称为下部分;其他的圆盘看作一个整体作为另一部分,称为上部分。以在五层汉诺塔中将A柱上的五层圆盘移动到C柱上为例,将A柱上的五层圆盘分成两个部分,其中将5圆盘(即最大的那一个圆盘)作为一部分,称为下部分;其他圆盘(即1-4圆盘,共四个圆盘)看作一个整体作为另一部分,称为上部分;五个圆盘要想从A柱(此时A柱为开始柱)上移动到C柱(此时C柱为目标柱)上,则上部分一定要放在B柱上(此时B柱为中转柱),A柱上就剩下 下部分了,就可以将下部分移动到C柱上,然后再将在B柱的上部分直接移动到C柱上就大功告成了。从这可以看出,在成功将A柱上五个圆盘移动到C柱上之前,上部分(其他四个圆盘)一定是放在中转柱上。
  3. 在上述第2部分中,要将其他圆盘(即1-4圆盘,共四个圆盘)放在B柱上,则问题变成从A柱移动四个圆盘到B柱上。其中将4圆盘(即第四个圆盘)作为一部分,称为下部分;其他圆盘(即1-3圆盘,共三个圆盘)看作一个整体作为另一部分,称为上部分;四个圆盘要想从A柱(此时A柱为开始柱)上移动到B柱(此时B柱为目标柱)上,则上部分一定要放在C柱上(此时C柱为中转柱),A柱上就剩下 下部分了,就可以将下部分移动到B柱上,然后再将在C柱的上部分直接移动到B柱上就大功告成了。从这可以看出,在成功将A柱上四个圆盘移动到B柱上之前,上部分(其他三个圆盘)一定是放在中转柱上。以此类推。

结合代码来理解

	tower.move(5, 'A', 'B', 'C');
	//num 表示要移动圆盘的个数,a,b,c只是变量名,本身不表示任何的柱,只有在参数传递时,传进什么柱便表示什么柱,例如
	//move(4, 'A', 'C', 'B'),则a表示A柱,b表示C柱,c表示B柱。
	public void move(int num , char a, char b ,char c) {
    //在move(int num , char a, char b ,char c)中,a为开始柱,b为中转柱,c为目标柱。
		//如果只有一个盘 num = 1
		if(num == 1) {
            //从开始柱移动到目标柱
			System.out.println(a + "->" + c)
		} else {
			//如果有多个盘,可以看成两个部分,最下面的一个盘看作一部分,称为下部分和上面的所有盘(num-1)看作另一部分,称为上部分。
			//(1)先移动上面所有的盘从a(开始柱)借助c(中转柱)移动到b(目标柱),注意在move(num - 1 , a, c, b)中,a为开始柱,
           //c为中转柱,b为目标柱。
			move(num - 1 , a, c, b);
			//(2)把最下面的这个盘从a(开始柱),移动到c(目标柱),此时的a(开始柱)和c(目标柱)是move(int num , char a, char   			
			//b ,char c)中的a(开始柱)和c(目标柱)。
			System.out.println(a + "->" + c);
			//(3)再把b(开始柱)的所有盘借助a(中转柱)移动到c(目标柱),注意在move(num - 1, b, a, c)中,b为开始柱,a为中转柱,
           //c为目标柱
			move(num - 1, b, a, c);
		}
	}

​函数:move(int num,char a,char b,char c),第一个参数表示移动圆盘的个数,第二个参数表示开始柱,第三个参数表示中转柱,第四个参数表示目标柱,这里需要特别注意的是,无论第二个参数传进来的是A柱或者B柱或者C柱,都表示是开始柱,无论第二个参数传进来什么内容,第二个参数都是表示开始柱,第三个参数和第四参数也是同样的道理。如:例如 move(3,‘A’, ‘B’, ‘C’) 表示将3个圆盘从A柱(开始柱)借助B柱(中转柱)移动到C柱(目标柱)。

​从A柱上将五层圆盘移动到C柱这个问题(将A柱上的五层圆盘分成两个部分,其中将5圆盘(即最大的那一个圆盘)作为一部分,称为下部分;其他圆盘(即1-4圆盘,共四个圆盘)看作一个整体作为另一部分,称为上部分),我们化繁为简,分成三步来执行。在else方法体中的三条执行语句分别表示一步,第一步:move(num - 1 , a, c, b)负责将上部分移动到中转柱上。第二步:System.out.println(a + “->” + c)负责将下部分移动到目标柱上。第三歩:move(num - 1, b, a, c)负责将中转柱的上部分移动到目标柱上。

递归过程

​ 重要的事情再说一遍,

  1. 函数:move(int num,char a,char b,char c),第一个参数表示移动圆盘的个数,第二个参数表示开始柱,第三个参数表示中转柱,第四个参数表示目标柱。
  2. 从A柱上将五层圆盘移动到C柱这个问题,我们化繁为简,分成三步来执行。第一步:move(num - 1 , a, c, b)负责将上部分移动到中转柱上。第二步:System.out.println(a + “->” + c)负责将下部分移动到目标柱上。第三歩:move(num - 1, b, a, c)负责将中转柱的上部分移动到目标柱上。

在理解下面递归过程中要是有迷糊的,记得重温上述两点。

第一步的递归过程:

圆盘移动个数 开始柱 中转柱 目标柱

move( num, a , b , c )

move( 5 , ‘A’ , ‘B’ , ‘C’ ):

​将A柱(a,开始柱)上的五个圆盘移动到C柱(c,目标柱),则先得将上面的四个圆盘移动到B柱(b,中转柱)。

move( 4 , ‘A’ , ‘C’ , ‘B’ ):

​将A柱(a,开始柱)上的四个圆盘移动到B柱(c,目标柱),则先得将上面的三个圆盘移动到C柱(b,中转柱)。

move( 3 , ‘A’ , ‘B’ , ‘C’ ):

​将A柱(a,开始柱)上的三个圆盘移动到C柱(c,目标柱),则先得将上面的两个圆盘移动到B柱(b,中转柱)。

move( 2 , ‘A’ , ‘C’ , ‘B’ ):

​将A柱(a,开始柱)上的两个圆盘移动到B柱(c,目标柱),则先得将上面的一个圆盘移动到C柱(b,中转柱)。

move( 1 , ‘A’ , ‘B’ , ‘C’ ):

​num == 1,到达递归的结束条件,将A柱(a,开始柱)上的最上面的圆盘(即1圆盘)移动到C柱(c,目标柱),开始执行递归回溯。

​从上述第一步的递归中可以得出:move(num - 1 , a, c, b)的实际是将上面的num - 1个圆盘移动到中转柱之前,一步步递归,计算出成功将上面的num - 1个圆盘移动到中转柱的圆盘移动路线和最小圆盘(即1圆盘)首先该移动到A柱B柱C柱中的哪一根柱(这里1圆盘是首先移动到C柱)。

递归回溯到System.out.println(a + “->” + c),此时最上面的圆盘(即1圆盘)已经移动到C柱,开始执行第二步的递归过程。

第二步的递归过程:

System.out.println(a + “->” + c);

将A柱(a,开始柱)上的2圆盘移动到B柱(c,目标柱),开始执行第三歩的递归过程。

第三歩的递归过程:

move( 1 , ‘C’ , ‘A’ , ‘B’ ):

​num == 1,到达递归的结束条件,将C柱(a,开始柱)上的最上面的圆盘(即1圆盘)移动到B柱(c,目标柱),开始执行递归回溯。

递归回溯到System.out.println(a + “->” + c),此时最上面的两个圆盘(即1圆盘和2圆盘)已经移动到B柱,开始执行第二步的递归过程。

第二步的递归过程:

System.out.println(a + “->” + c);

将A柱(a,开始柱)上的3圆盘移动到C柱(c,目标柱),开始执行第三歩的递归过程。

第三歩的递归过程:

move( 2 , ‘B’ , ‘A’ , ‘C’ ):

将B柱(a,开始柱)上的两个圆盘(即1圆盘和2圆盘)移动到C柱(c,目标柱),则先得将上面的一个圆盘移动到A柱(b,中转柱),开始执行第一步的递归过程。

​以此类推,其他的递归过程就不再写了。

​从上述第三步的递归中可以得出:move(num - 1 , a, c, b)的实际是将中转柱的num - 1个圆盘移动到目标柱之前,又回到通过第一步的递归将上面的num - 2个圆盘移动到中转柱之前,一步步递归,计算出成功将上面的num - 2个圆盘移动到中转柱的圆盘移动路线和最小圆盘(即1圆盘)首先该移动到A柱B柱C柱中的哪一根柱的套路上。

​我已经将我在汉诺塔问题上觉得比较难理解的部分都尽量说得详细和通俗易懂一点,可能有点啰嗦,但希望能够帮助到大家理解,也欢迎大家在评论区一起讨论。

  • 9
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
请看以下代码: ```c #include <stdio.h> void move(int n, char A, char B, char C) { if (n == 1) { printf("%c -> %c\n", A, C); return; } move(n - 1, A, C, B); printf("%c -> %c\n", A, C); move(n - 1, B, A, C); } int main() { int n; printf("请输入汉诺塔的层数:"); scanf("%d", &n); move(n, 'A', 'B', 'C'); return 0; } ``` 注意,这是使用递归来解决汉诺塔问题代码,如果你希望使用非递归解决汉诺塔问题,可以参考以下原理: 使用非递归方法解决汉诺塔问题可以使用栈来模拟递归过程。首先,建立一个栈,不断将问题压入栈中,直到问题被解决。具体步骤如下: 1.定义一个结构体,包含三个成员,分别代表盘子的大小、起始柱子和目标柱子。 2.按照递归的思路,将问题划分为多个子问题,并分别压入栈中,直到问题被分解到只有一个盘子时停止。 3.不断从栈中弹出问题,并依次解决它们,直到栈为空。对于每个问题,首先判断是否已经无法再分解,如果是,则直接移动盘子;如果不是,则先将子问题按照从小到大的顺序依次压入栈中。 下面是使用非递归方法解决汉诺塔问题代码: ```c #include <stdio.h> #include <stdlib.h> struct StackNode { int n; // 盘子的大小 char from, to; // 起始柱子和目标柱子 int step; // 步数 }; struct Stack { struct StackNode *data; // 栈中存储的数据 int max_size; // 栈的大小 int top; // 栈顶位置 }; void init_stack(struct Stack *s, int max_size) { s->data = (struct StackNode *) malloc(max_size * sizeof(struct StackNode)); s->max_size = max_size; s->top = -1; } void push(struct Stack *s, struct StackNode *x) { if (s->top == s->max_size - 1) { printf("Stack is full!\n"); return; } s->top++; s->data[s->top] = *x; } void pop(struct Stack *s) { if (s->top == -1) { printf("Stack is empty!\n"); return; } s->top--; } struct StackNode *top(struct Stack *s) { if (s->top == -1) { printf("Stack is empty!\n"); return NULL; } return &(s->data[s->top]); } int is_stack_empty(struct Stack *s) { return s->top == -1; } void move(int n, char from, char to, int step) { printf("%c -> %c, Step %d\n", from, to, step); } void hanoi(int n, char from, char to) { struct Stack s; init_stack(&s, 100); struct StackNode node; node.n = n; node.from = from; node.to = to; node.step = 0; push(&s, &node); while (!is_stack_empty(&s)) { struct StackNode *cur = top(&s); if (cur->n == 1) { move(1, cur->from, cur->to, cur->step + 1); pop(&s); } else if (cur->n > 1) { if (is_stack_empty(&s)) { printf("Stack is empty!\n"); break; } struct StackNode *next = top(&s); if (next->n != cur->n - 1) { node.n = cur->n - 1; node.from = cur->from; node.to = get_other_peg(cur->from, cur->to); node.step = cur->step; push(&s, &node); } else { move(1, cur->from, cur->to, cur->step + 1); pop(&s); if (!is_stack_empty(&s)) { next = top(&s); if (next->n != cur->n - 1) { node.n = cur->n - 1; node.from = get_other_peg(cur->from, cur->to); node.to = cur->to; node.step = cur->step + 1; push(&s, &node); } else { move(1, cur->to, next->to, cur->step + 2); pop(&s); } } else break; } } else break; } } int main() { int n; printf("请输入汉诺塔的层数:"); scanf("%d", &n); hanoi(n, 'A', 'C'); return 0; } char get_other_peg(char a, char b) { switch (a) { case 'A': if (b == 'B') return 'C'; if (b == 'C') return 'B'; case 'B': if (b == 'A') return 'C'; if (b == 'C') return 'A'; case 'C': if (b == 'A') return 'B'; if (b == 'B') return 'A'; } return ' '; } ``` 这里使用了一个栈来模拟递归过程,具体实现可以参考代码注释。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值