1.函数的递归调用

函数可以直接或者间接的调用其自身,这称为函数的递归调用。递归算法的实质是将原有的问题逐层拆解为新的问题,而解决新的问题又用到了原问题的解法,因此可以继续调用自身分解,按照此原则一直分解下去,每次出现的新问题都是原有问题的子集(或者说是简化版的原问题),而最终的最终分解出来的最后一个问题,一定是已知解的问题,否则没有意义。

因此递归过程都可以用以下两个阶段概括。

Step1:递推。所谓递推就是将原有的问题不断拆解为新的子问题,其子问题就是原有问题的弱化(或者说更少层)的问题,这样,逐渐从未知向已知推进,最终的一个子问题一定是个已知的问题,到达此后,递归阶段结束。

Step2:回归。上一阶段递归阶段结束后,进入回归阶段,所谓回归就是从上一阶段的已知的问题出发,按照递推的过程,逐一求值回归,逐渐将未知问题都变成已知问题,一直到达递推的最开始处,结束回归阶段,问题解决。

2.经典递归调用—汉诺塔问题

2.1 问题描述

有三根针A,B, C。A针上有n个盘子,盘子大小不等,大的在下,小的在上,如下图示,要把这n个盘子从A针移动到C针,在这个过程中可以借助B针,每次只允许动一个盘子,而且在移动的过程中,这三根针上都保持大盘子在下,小盘子在上。

汉诺塔java递归流程图 汉诺塔递归程序讲解_递归调用

图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个盘子,问题解决。

步骤如图所示:

汉诺塔java递归流程图 汉诺塔递归程序讲解_汉诺塔java递归流程图_02

步骤2.1- 先把2个盘子从A移动到B

 

汉诺塔java递归流程图 汉诺塔递归程序讲解_递归_03

步骤2.2- 把A上的最后一个盘子移动到C

 

汉诺塔java递归流程图 汉诺塔递归程序讲解_递归_04

步骤2.3- 把B上的2个盘子移回C

 

具体细分到每一步的Gif动画演示如下:

汉诺塔java递归流程图 汉诺塔递归程序讲解_递推_05

三层汉诺塔动画演示

 

Show me the code!