1.函数的递归调用
函数可以直接或者间接的调用其自身,这称为函数的递归调用。递归算法的实质是将原有的问题逐层拆解为新的问题,而解决新的问题又用到了原问题的解法,因此可以继续调用自身分解,按照此原则一直分解下去,每次出现的新问题都是原有问题的子集(或者说是简化版的原问题),而最终的最终分解出来的最后一个问题,一定是已知解的问题,否则没有意义。
因此递归过程都可以用以下两个阶段概括。
Step1:递推。所谓递推就是将原有的问题不断拆解为新的子问题,其子问题就是原有问题的弱化(或者说更少层)的问题,这样,逐渐从未知向已知推进,最终的一个子问题一定是个已知的问题,到达此后,递归阶段结束。
Step2:回归。上一阶段递归阶段结束后,进入回归阶段,所谓回归就是从上一阶段的已知的问题出发,按照递推的过程,逐一求值回归,逐渐将未知问题都变成已知问题,一直到达递推的最开始处,结束回归阶段,问题解决。
2.经典递归调用—汉诺塔问题
2.1 问题描述
有三根针A,B, C。A针上有n个盘子,盘子大小不等,大的在下,小的在上,如下图示,要把这n个盘子从A针移动到C针,在这个过程中可以借助B针,每次只允许动一个盘子,而且在移动的过程中,这三根针上都保持大盘子在下,小盘子在上。
图2-1 汉诺塔问题示意图
2.2 解决思路
实际上,正如上面所说,这是一个典型的递归调用。要解决这个问题,本质上还是以上所说的递归的两个阶段。
Step1:递归
①:从问题出发,我们需要达到的目标是什么:我们是想要将A上的n个盘子移动到C上
②:为了解决①,我们可以这么做:
(2.1)我们可以把n-1个盘子从A针(1至n-1号盘)移至B针;
(2.2)之后将A针中剩下的第n号盘(最下面那个)移至C针;
(2.3)最后我们再把第2.1步中那n-1个盘子从B针全部移回至C针,至此,C针上有n个盘子,问题解决。
经过以上三个步骤,我们把想要解决的目标问题拆分成了3个新的子问题,且这3个新的子问题都是原有的问题的弱化版或者说低层版本,我们只需要按照此一直递归,最后找到一个已知的问题即可,下面我们继续递归。
③:
针对问题2.1,也就是将n-1个盘子从A针移动到B针,这个问题熟悉不熟悉?没错,这个问题他不就是我们要解决的问题(从A->C移动n个盘子)换了个目标盘(变成了从A->B)还有减少了一次次数(移动n-1个盘子)的弱化版本么?所以这个问题本身又可拆分成3个新的子问题,现在我们的目标是:我们是想要将A上的n-1个盘子移动到B上。为了解决这个问题,我们可以这么做:
(3.1)我们可以把n-2个盘子A针(1至n-2号盘)移至C针;
(3.2)之后将A针中剩下的第n-1号盘(最下面那个)移至B针;
(3.3)最后我们再把第3.1步中那n-2个盘子从C针全部移回至B针,至此,B针上有n-1个盘子,问题解决。
经过以上3个步骤,问题进一步弱化了,如3.1所示,需要把n-2个盘子A针(1至n-2号盘)移至C针,我们只需要移动n-2个盘子即可。这个问题熟悉不熟悉?没错,这就是我们要解决的问题(从A->C移动n个盘子),次数减少了两次次数(移动n-2个盘子)的进一步弱化版本么?看到这里相信你已经找到了规律。
所以,要解决问题3.1我们可以进一步拆分成3个新的子问题,如此反复,就是一个Step1不断递推的过程,直到最后问题被拆分为:需要移动1个盘子从起始针->目标针,此时直接移动即可。这也就是所谓的递推的最后即最终的子问题变成了一个已知问题,递推阶段便结束了。
递推阶段结束后,便是逐层回归,从已知问题出发,每个n.1问题解决后,顺序解决n.2,n.3,直到回到最开始处,结束回归。比如上述3.1解决后,3.2可直接移动,3.3又可拆分成3个新的子问题,一直拆分到出现已知问题,层层回归解决3.3问题。
我们是为了解决2.1问题才拆分成3.1,3.2,3.3问题,那么3.1,3.2,3.3问题都解决后,也就是2.1问题被解决了,继续解决2.2问题,2.2问题可直接移动,问题解决。继续解决2.3问题,还是不断递归拆分直到出现已知问题,再不断回归解决问题2.3.
我们是为了解决我们最初的1问题也就是最初的目标问题才拆分成2.1,2.2,2.3问题的,因此,2.1,2.2,2.3问题都解决后,最初的1问题也就解决了。至此全部结束。
2.3 C++代码编写
代码写起来很简单,首先定义一个最基本的操作:移动盘子
从A移动一个盘子到C,起始针A称为src原盘,终点盘C称为dst目标盘,那么便有:
//移动src最上面一个盘子到dst针
void move(char src, char dst)
{
cout << src << "--》" << dst << endl; //表示将src的一个盘子移动到了dst盘
}
接下来我们再定义一个函数:从原盘移动n个盘子到目标盘
起始针称为src原盘,借助中间盘称为medium中间盘,终点盘称为dst目标盘,那么便有:
//代表从src->dst(借助medium)移动n个盘子
void hanoi(int n,char src,char medium,char dest)
知道以上代表的含义后,我们就可以开始编写代码解决问题了。
经过2.2章节的分析我们知道,要解决①问题,我们拆分成了3个子问题2.1,2.2,2.3,我们分别看下这三个子问题:
目标问题:将n个盘子从A针移到C针
分解步骤1:我们先把n-1个盘子从A针(1至n-1号盘)移至B针(借助C针);
这个问题写成代码就是:
//将n-1个盘子从A针(借助C针)移动到B针上
hanoi(int n-1,A,C,B);
因为刚开始传入参数时候,赋值的A是src针,B是medium针,C是dst针,所以可以写成:
//将n-1个盘子从A针(借助C针)移动到B针上
hanoi(int n-1,src,dst,medium);
分解步骤2:之后将A针中剩下的第n号盘(最下面那个)移至C针;
这个问题写成代码就是:
//将A针中剩下的第n号盘(最下面那个)移至C针
move(A,C);
因为刚开始传入参数时候,赋值的A是src针,C是dst针,所以可以写成:
//将A针中剩下的第n号盘(最下面那个)移至C针
move(src,dst);
分解步骤3:最后我们再把那n-1个盘子从B针全部移至C针(借助A针);
这个问题写成代码就是:
//将n-1个盘子从B针(借助A针)移动到C针上
hanoi(int n-1,B,A,C);
因为刚开始传入参数时候,赋值的A是src针,B是medium针,C是dst针,所以可以写成:
//将n-1个盘子从B针(借助A针)移动到C针上
hanoi(int n-1,medium,src,dst);
当最终n=1时,我们只需要移动move(src,dst)即可,这也是最终的已知问题。
因此,完整的代码为:
void move(char src, char dst)
{
cout << src << "--》" << dst << endl;
}
void hanoi(int n, char src, char medium, char dst)
{
if (n==1) //n==1代表最终的已知问题
{
move(src, dst);
}
else
{
hanoi(n - 1, src, dst, medium); //子问题2.1,继续调用自身
move(src, dst); //子问题2.2
hanoi(n - 1, medium, src, dst); //子问题2.3,继续调用自身
}
}
int Exam_3_10()
{
int n;
cout << "请输入盘子个数:" << endl;
cin >> n;
hanoi(n, 'A', 'B', 'C');
return 0;
}
2.4 3层汉诺塔动画演示
举个简单的例子,假设A上有3个盘子1,2,3盘,也就是3层汉诺塔问题。
目标问题:我们需要将3个盘子从A针移动到C针(借助B)
分解问题:
(2.1)我们可以先把2个盘子从A针(1、2号盘)移至B针;
(2.2)之后将A针中剩下的第3号盘移至C针;
(2.3)最后我们再把第2.1步中那2个盘子从B针全部移回至C针,至此,C针上有3个盘子,问题解决。
步骤如图所示:
步骤2.1- 先把2个盘子从A移动到B
步骤2.2- 把A上的最后一个盘子移动到C
步骤2.3- 把B上的2个盘子移回C
具体细分到每一步的Gif动画演示如下:
三层汉诺塔动画演示
Show me the code!