zoukankan      html  css  js  c++  java
  • 递归调用——数学观点看递归

                
            想起上大学那会儿递归调用曾是那么令人头痛,现在工作也近两年时间了,对递归倒是有了较明晰的了解. 
    递归,数学里面叫recursion,其实就是递推关系. 中学数学有一部分其实就是递归的非常典型的做法,不过老师们都没怎么扩展,新课标必修五第二章数列应该算是我们第一次接触递推的概念了. 
            其实说到递归,大伙都知道就是自己调自己,这样其实大家都明白,但是说来怎么调?如何控制?又如何看得到结果是想要的呢?相信还是很晕,下面从中学数学里面来看看吧. 

            第一部分、两个典型的例子,等差数列与等比数列

           其实这实际上是一个例子,什么是等差数列?就是后一项总比前一项多一个数,这个数不变...,也有的说:“就是后一项减去前一项为一定常数...”
           所以我们经常用表达式表示为
           a(n)=a(n-1)+d]
          那又有问题了,这里确定了一个数列没有呢?当然没有,我说后一项比前一项多2,这是什么数列?
           是1,3,5,7,9, ...还是2,4,6,8,10, ...
           当然弄不清楚,为什么呢?因为我们谈到的数列需要有一个首项,即第一项的值,所以一旦谈到解递推数列,就应该有两个内容,一个是连续项间的关系,另一个就是首项关系. 

          那么就可以利用叠加的方法来计算了:
    假设这里首项为1,也就是a(1)=1,而这个常数就为2
    那么
    a(n)=a(n-1)+2
    a(n-1)=a(n-2)+2
    a(n-2)=a(n-3)+2
    a(n-3)=a(n-4)+2
    ...
    a(3)=a(2)+2
    a(2)=a(1)+2
    将等号左边的依次相加,右边的也依次相加
    这样很容易发现,左边有一部分从a(2)一直加到a(n-1),右边也有一部分从a(2)一直加到a(n-1),那么消去,就左边剩下a(n),右边就剩下a(1)和n-1个2相加了,数学公式就成了
    a(n)=a(1)+2(n-1)

    a(n)=1+2(n-1)
    就是等差数列的通向公式啦,那么这个递推关系中可以看到a(n)=a(n-1)+x即为连续项间的关系,而a(1)=1就是首项啦. 那这个和递归间的关系就很明显啦
    a(n)=a(n-1)+x表示,调用a(n)即执行a(n-1)+x,那么同时调用a(n-1),...这样一直下去,最后当a(1)时不在调用函数,直接返回1,再一路加回去,结束递归. 

    下面看看用代码来实现,还是假定首项为1,公差为2

    #include <stdio.h>
    int main()
    {
    int AddFun(int);
    int num;
    printf("Please input num:");
    scanf("%d",&num);
    printf("The result is %d",AddFun(num));
    return 0;
    }


    int AddFun(int n)
    {
    if(n == 1)
    {
    return 1;
    }
    return AddFun(n-1) + 2;
    }
    这里呢AddFun(n)就是之前看到的a(n),在调用的过程中先判断n是否为1,如果为1当然就表示现在是首项了,不在继续调用,直接返回结果1
    这里使用的不是数学中的公式计算,纯粹的事利用递推关系得到的结果. 


    再看看等比数列. 相信已经很清楚了,等比数列就是后一项是前一项的定常数被,表示为
    a(n)=a(n-1)*q
    LaTeX代码为
    [a_n=a_{n-1}cdot q]
    将函数改一改就能使用了,假设首项为1,公比为2:

      static void Main(string[] args)
            {
    
                Console.Write("Please input num:");
                int n = int.Parse(Console.ReadLine());
                int result = AddFun(n);
                Console.WriteLine(result);
                Console.ReadKey();
            }
            private static int AddFun(int n)
            {
                if (n == 1)
                {
                    return 1;
                }
                else
                {
                    return AddFun(n - 1) * 2;
                }
            }

           当然递归不一定要调用函数开完成,使用for循环一样可以做到

         
           第二部分、递归调用注意(直接递归)


    递归调用在编程里面就是对一个函数,或方法进行自己掉自己,那么在编写递归代码时有三点要注意,首先要注意一个问题,就是递推关系,这里需要抽象出一个递推的关系,不一定只有两层,或许是三层甚至更多

    例如:楼梯有n阶台阶,上楼可以一步上1阶,也可以一步上2阶,编一程序计算共有多少种不同的走法.

    先来分析一下,如果只有一个台阶,那么显然只有1种,所以a(1)=1
    如果有两个台阶,呢么就有每次上一层,上两次,和一次上两层,即1+1和2共2种,即a(2)=2
    当n=3时,那么就有1+1+1、1+2、2+1共3种了,似乎还看不出规律,那么分析一下递推关系,即前后项间的关系
    到达第三层有两个方法,一是从第二层走一步到第三层,那么有a(2)种方法,又可以从第一层走两步到第三层,即a(1)种方法,拍脑袋了,显然到达第三层有a(1)+a(2)种方法,那么是不是呢?验证一下
    a(1)+a(2)=1+2=3,咦,正好,看来有点像了,再来看看第四层是不是
    当n=4时,按照上边的分析肯定为a(2)+a(3)=2+3=5,下面我们枚举一下:1+1+1+1、1+1+2、1+2+1、2+1+1、2+2,刚刚好,看来就是她了!
    那么递推关系就有了,即a(n)=a(n-1)+a(n-2),同时还有了首项的值:a(1)=1,a(2)=2,由于递推关系有三项,很显然必须有两个初始值

    好啦,上面讨论了递归的一个注意,下面是第二个注意,临界条件
    既然递归就是自己调自己,那么怎么停止呢,显然不做逻辑判断,计算机会一直运行下去,知道程序崩溃(栈溢出),所以就需要加上一个if判断来跳出递归,其实就是前面提到的数列首项,就是这里的a(1)和a(2)
    那么代码就可以这么写了:

      private static int Count(int n)
            {
    
                if (n == 2)
                {
                    return 2;
                }
                else if (n == 1)
                {
                    return 1;
                }
                else
                    return Count(n - 1) + Count(n - 2);
    
            }


    实际上这就是一个典型的Fibonacci数列,只是初始值不同罢了。

    现在剩下第三个要注意的事项了,就是递归体. 
    先看两个函数代码(C++代码):

    void Test_1(int n)
    {
    cout<<n<<endl;
    if(n < 6)
    {
    Test_1( n + 1 );
    }
    }
    2void Test_2(int n)
    {
    if(n < 6)
    {
    Test_2( n + 1 );
    }
    cout<<n<<endl;
    }


    如果在main()函数中分别调用这两个函数,会有什么执行结果呢?
    现在我们假定n为0
    第一个函数打印出
    0
    1
    2
    3
    4
    5
    6
    第二个函数打印出
    6
    5
    4
    3
    2
    1
    0
    这个就很奇怪了,这是为什么呢?
    实际上前面我们考虑的递归关系是单一的递归,但是这两个递归体中,除了完成递归外,还有其他执行代码. 
    其实之前也有,只是没有显示出来,就没有被察觉而已
    那么怎么去分析呢?
    其实很简单,递归体本身可分为三个部分,一个是头代码,一个是递归表达式,一个是尾代码,区分头尾的自然就是递归表达式了
    看这段代码:

    #include <iostream>
    using namespace std;
    int main()
    {
        void Test(int);
        Test(1);
        return 0;
    }
    void Test(int n)
    {
        cout<<n<<endl;
        if(n < 3)
        {
    Test( n + 1 );
    cout<<n<<endl;
        }
    }

    输出结果为
    1
    2
    3
    2
    1

    那么在这段代码中if()前面的输出流为头代码,中间的Test( n + 1 )为递归表达式,这里的尾代码依然是一个输出流
    那么分析此程序怎么做呢?很简单,八个字
    “头尾分开,等待归来”
    头尾分开,就是将两个cout<<n<<endl分开,第一个cout<<n<<endl执行,然后转入第一次递归,等待递归结束后回到第二个cout<<n<<endl语句执行
    这样说很抽象,我们一步步分析一下

    第一次调用,n=1
    执行头cout<<n<<endl,输出1,并且尾cout<<n<<endl等待,其值也是1,但未输出;
    进入第一次递归,n=2
    执行头cout<<n<<endl,输出2,并且尾cout<<n<<endl等待,其值是2,但未输出;
    进入第二次递归,n=3
    执行头cout<<n<<endl,输出3,但判断n < 3 不成立,即不再执行尾cout<<n<<endl语句,函数体结束
    回到第一次递归体,执行等待的尾cout<<n<<endl,输出2,第一次递归体结束,会带最外层
    执行等待的cout<<n<<endl,输出1,整个函数调用结束,回到main()函数体继续执行其他命令.

    形象的说就像是一架手风琴一样,函数调用开始就拉开手风琴,然后进入递归,等待内层递归结束才执行后面的代码
    也就八个字“头尾分开,等待归来”
    其实这就是实现的栈的结构

    到这里就基本结束了我所理解的递归含义,总结一下:
    递归调用即递推关系
    递归三个注意:注意递推表达、注意临界判断、注意递归体

  • 相关阅读:
    ASP.NET AJAX 's UpdatePanel 中使用Page.ClientScript.RegisterStartupScript 失败
    从struct到byte[]之RawFormatter
    一个HTTP.二进制POST和HTTP指定长度接收的C++实现
    发现移动太NB了,验证码图像路径直接包含验证码
    又一个网页下载者木马
    Trojan program TrojanDownloader.JS.IstBar.ai 病毒样本
    用控件的方式解决问题在客户端关联WEB控件引用
    需要防范的XSS攻击
    用"指定字符组成的任意进制"转换生成不带4的卡号
    人为漏洞的构造、文件的载入、验证机制的突破
  • 原文地址:https://www.cnblogs.com/a164266729/p/3289088.html
Copyright © 2011-2022 走看看