zoukankan      html  css  js  c++  java
  • 图解递归

    参考网址: 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上
    }

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

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

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

  • 相关阅读:
    SQL查询,所有的客户订单日期最新的前五条订单记录。
    session的原理
    @RequestMapping 注解用在类上面有什么作用?
    事务的概念,在JDBC编程中处理事务的步骤
    .Redis优势
    Spring框架中都用到了哪些设计模式?
    redis
    你为什么觉得自己能够在这个职位上取得成就?
    怎样理解团队?请举例并说明启示。
    物质待遇和工作条件是人们选择工作的重要因素之一,这次报考谈一谈你在选择工作时都考虑哪些因素?为什么?
  • 原文地址:https://www.cnblogs.com/bruce1992/p/15160577.html
Copyright © 2011-2022 走看看