图解递归

参考网址: https://zhuanlan.zhihu.com/p/88760014

递归可能是大多数编程初学者的第一道大坎,它本身并不困难,但是因为执行方式略微特别而一时摸不到头脑,其实适应了之后用起来也能得心应手,并且能更加方便的解决许多单纯用循环较难解决的问题。

很多讲述递归的文章中,把分析递归问题放在了第一位,着重讲解了分析递归问题,找出结束条件,递归关系等,但是忽略了程序本身的执行方式,在我看来分析问题与程序执行过程一样重要,两者缺一不可,只会分析问题,很容易写出有BUG的程序,同时也不易深入理解,只理解递归执行过程,不会分析问题,那就是纸上谈兵,实际上也什么都没学到。

这篇文章重点在于帮助理解递归程序本身的执行过程,其实递归函数和普通函数的调用并没有什么区别,所以先理解普通函数的调用,再理解递归就不难了。

举个例子,一个普通的调用

void funC()
{
    puts("funC");
}
void funB()
{
    funC();
    puts("funB");
}
void funA()
{
    funB();
    puts("funA");
}

假如调用函数funA,那么它就会调用函数funB,funB再调用funC,funC打印出"funC",此时funC执行完毕返回至funB继续执行,打印出"funB",然后funB执行完毕返回至funA,funA打印"funA",执行完毕,执行结果如图所示。

可以看出来,每次调用一次函数,都是等它彻底执行完毕之后,才会返回去继续执行,那么如果将每个函数“展开”,大概就像这样

每一个大框代表一次函数调用,这样可以很直观的看出程序执行过程(按图从上至下即可)。“每调用一次函数,都是等它彻底执行完毕之后,才会返回去继续执行”这句话的含义,体现在图中就是一个框被整个包裹在另一个框之中。

其实递归也是如此,从根本上来说,递归调用其实调用的并不是本身,你写在程序里的一个函数,调用它其实并不是真的执行了它,而是开辟了一份空间,把其中的数据(包括参数,和函数里的一些变量等),存在其中,然后根据你写的代码去修改这些数据,每次调用一个函数,就会开辟一份这样的空间,执行完毕之后释放,而递归的时候,每一次递归调用都会开辟一份这样的空间,互相独立,所以才有本段开头的一句话,所以,如果一次递归调用共调用了N次,你就可以理解为你写了N个函数,只不过它们长得一样罢了。

举个例子,经典的递归求阶乘,如果我们想要算出n!,只需要先算出(n-1)!再乘n就可以了(递归求阶乘其实是相当差的一个方法,这里仅做举例):

int fact(int n)
{
    if(n==0)return 1;
    else return fact(n-1)*n;
}

fact(4)的执行过程如下:

当计算fact(1)时,就是先算出了fact(0)=1,然后才能知道fact(1)是1*1=1。

当计算fact(2)是,也是先算出了fact(1)=1,然后才能知道fact(2)是1*2=2。

其他也类似。

从图中还可以看出来,每一次递归调用,都是彻底执行完这一次调用,才会返回(经过一个函数框的下边框就意味着返回了)。

再举一个例子,经典的河内塔问题:有三根柱子,编号分别是A,B,C,A柱子上有N个空心圆盘,圆盘大小互不相同,且最下面的最大,最上面的最小,由下向上大小递减放置,现在你需要把所有的圆盘移动至C柱子上,移动过程中每次只能挪动一个圆盘,且这个圆盘必须处于一个柱子的最上端,并且时刻保证大圆盘不能位于小圆盘的上面,求一个挪动方案,且该方案步数最少。

乍一看,这个问题似乎十分困难,但利用递归的思想可以轻易解决:

首先,我们用一个这样的二元组<x,y>表示一次从x到y的挪动(我们每次只能挪动x柱上最上面的圆盘,所以无需额外记录被挪动圆盘的编号),用若干个这样的组合表示一个挪动方案,那么,首先可以注意到的是,不论挪动方案是什么,其中必定有一步是<A,C>,表示把最大的圆盘从A挪到C上,这一步显然是必需的,那么为了挪出这一步,首先A柱上只能有最大的圆盘,然后C柱必须为空,此时剩下的N-1个圆盘都在B柱上,因为大小的限制,它们的顺序也被确定了。于是在挪最大圆盘之前,就是把N-1个盘子从A挪到B,挪完最大盘子之后,就是再把N-1个盘子从B挪到C上。

定义一个函数f(n,x,y,z),它会输出把n个圆盘从柱子x挪到柱子z上的方案,y柱子充当中转站,该问题只需要调用f(N,'A','B','C')即可解决。

显然,如果最大圆盘固定位置不动,那么它对其他圆盘的挪动没有影响,于是,N-1个盘子的挪动其实就是这个问题本身,只不过规模小了1,写成代码就是:

void f(int n,char x,char y,char z)
{
    if(n==0)return;          //没有盘子需要挪动,直接返回
    f(n-1,x,z,y);            //最大的盘子要挪到z上,上面的n-1个挪到y上给最大的圆盘让路
    printf("<%c,%c>
",x,z); //挪动最大的盘子
    f(n-1,y,x,z);            //再把那n-1个从y上挪到z上
}

以上代码写起来较为简洁自然,但是不利于图解(因为图会很啰嗦,平白多出一层),图解时应用以下代码

void f(int n,char x,char y,char z)
{
    if(n==1)
    {
        printf("<%c,%c>
",x,z); //只有一块圆盘,直接挪动即可
        return;
    }
    f(n-1,x,z,y);            //最大的盘子要挪到z上,上面的n-1个挪到y上给最大的圆盘让路
    printf("<%c,%c>
",x,z); //挪动最大的盘子
    f(n-1,y,x,z);            //再把那n-1个从y上挪到z上
}

这段代码跟上面的代码没有本质区别,只不过结束条件从没有盘子,变成了剩一个盘子。

上图为两个盘子的挪动方案

上图为三个盘子的挪动方案

原文地址:https://www.cnblogs.com/bruce1992/p/15160577.html