zoukankan      html  css  js  c++  java
  • C/C++ 递归

    递归

    当一个函数调用它自己来定义时称它为递归函数。(什么叫它自己调用它自己呢?)

    1.1、引出递归

    从一个简单的问题考虑递归,求0,1,2, 3,4,5......n的和。

    首先定义一个求和公式:sum(n);

    显然对于(n > 0): sum(n) = sum(n - 1) + n ;

    ​ (n = 0 ) : sum(0) = 0;

    ​ 成立。

    将上述公式翻译成C++函数:

    unsigned int sum(unsigned int n)
    {
        if(0 == n)
        {
            return 0; //基准情况(递归的出口),sum不能一直调用它自己吧,总归要有一个出口结束递归吧
        }
        else
        {
            return sum(n - 1) + n; //sum(unsigned int)调用了它自己
        }
    }
    
    

    假设 n = 5 分析一下计算过程:

    sum(5) = sum(4) + 5;

    sum(4) = sum(3) + 4;

    sum(3) = sum(2) + 3;

    sum(2) = sum(1) + 2;

    sum(1) = sum(0) + 1;

    sum(0) = 0; 当sum(0)时,sum()不再调用它自己,作为递归的出口结束递归。

    假设没有n = 0, sum(0) = 0 这个基准情况作为递归的出口跳出递归,递归就会一直递归下去,没完没了直至崩溃。因此递归函数必须有一个基准情况作为递归出口

    1.2、失败的递归

    给出一个所谓的递归函数:

    int bad(unsigned int n)
    {
        if(0 == n)
        {
            return 0;
        }
        else
        {
            return bad(n/3 + 1) + n - 1;
        }
    }
    

    分析一下以上函数,函数给出了 n = 0 的情况作为递归的出口,看似没什么问题。

    还是假设n = 5;

    bad(5) : 调用bad(5/3 + 1), 即bad(2);

    bad(2) : 调用bad(2/3 + 1), 即bad(1);

    bad(1) : 调用bad(1/3 + 1), 即bad(1);

    bad(1) : 调用bad(1/3 + 1), 即bad(1)..........

    bad(1)一直调用bad(1), 一直调用到程序崩溃。很明显bad()函数定义虽然给出了 n = 0 作为递归出口,但是bad()函数根本不会推进到n = 0 的这种情况。因此递归调用必须总能够朝着产生基准情况(递归出口)的方向推进

    1.3、递归和归纳

    考虑一个问题:现在需要将一个正整数 n 打印出来,但是I/O给出的函数接口(printDigit)只能处理单个数字(即n < 10)。

    我们随便假设一个n值:n = 2019,那么单个数字打印的顺序就是2, 0, 1, 9。换句话说,9是最后一个打印的,在打印9之前要先打印201,即先打印“201”,再打印“9”;依次类推对于“201”先打印“20”,再打印“1”;对于“20”先打印“2”,再打印“0”;对于2已经是单个数字,可以直接打印了, 不需要再划分,再递归了,也就是说单个数字n < 10即为递归的出口。

    我们按上述思路细致的分析一下:

    对2019分成2部分: 201 = 2019 / 10; 9 = 2019 % 10;

    对201分成2部分:20 = 201 / 10; 1 = 201 % 10;

    对20分成2部分:2 = 20 / 10; 0 = 20 % 10;

    对于 2 满足 n < 10 的条件,不再递归,直接打印。

    现在递归已经很明显了,尝试编写一下代码:

    //假设printDigit((unsigned int n)如下,
    void printDigit(unsigned int n)
    {
        std::cout << n;
    }
    
    void print(unsigned int n)
    {
        if(n >= 10)
        {
            print(n / 10);
        }
        printDigit(n % 10);
    }
    

    代码编写好了,现在需要证明以下代码是否正确:对于n >= 0,数的递归打印算法总是正确的。

    证明:用k表示数字n的包含单个数字的个数。当k = 1,即 n < 10 时,很明显程序是正确的,因为它不需要递归,print()只调用一次printDigit(), 不调用它自己。然后假设print()对于所有k位数都能正常工作,任何k + 1位的数字n都可以通过它的前k位的数字和最低1位数字来表示。前k 位的数字恰好是[ n / 10], 归纳假设它能正常工作,而最低1位数字是[ n % 10],因此该程序能够正确的打印出任意k + 1位。于是根据归纳法[1],所有数字都能被正确打印出来。

    由以上实例总结可以出一条递归的设计法则:假设所有递归调用都能运行。

    1.4、递归的合成效益法则

    用递归实现一个斐波那契数列:

    //斐波纳契数列:1、1、2、3、5、8、13、21、34
    int f(int n)
    {
        if(n < 1)
        {
            return 0;
        }
        else if(n <= 2)
        {
            return 1;
        }
    
        return f(n-1) + f(n-2);
    
    }
    

    假设n = 8, 函数调用f(8), 递归调用如下图:

    graph TB 8-->7; 7-->6; 6-->5; 5-->4; 4-->3; 3-->2; 8-->id0(6); id0(6)-->id1(5); id1(5)-->id2(4); id2(4)-->id3(3); id3(3)-->id4(2); 7-->id5(5); id5(5)-->id6(4); id6(4)-->id7(3); id7(3)-->id8(2); 6-->id9(4); id9(4)-->id10(3); id10(3)-->id11(2); 5-->id12(3); id12(3)-->id13(2); 4-->id14(2); 3-->id15(1); id12(3)-->id16(1); id9(4)-->id17(2); id10(3)-->id18(1); id5(5)-->id19(3); id19(3)-->id20(2); id19(3)-->id21(1); id6(4)-->id22(2); id7(3)-->id23(1); id0(6)-->id24(4); id24(4)-->id25(3); id24(4)-->id28(2); id25(3)-->id26(2); id25(3)-->id27(1); id1(5)-->id29(3); id29(3)-->id30(2); id29(3)-->id31(1); id2(4)-->id32(2); id3(3)-->id33(1);

    由上图我们不厌其烦的数一下:

    n = 1时,f()调用1次;

    n = 2时,f()调用1次;

    n = 3时,f()调用3次;

    n = 4时,f()调用5次;

    n = 5时,f()调用9次;

    n = 6时,f()调用15次;

    n = 7时,f()调用25次;

    n = 8时,f()调用41次;

    增长的是不是太快了,在f()里加一个计数器测试一下,可以看到在n = 30 的时候,f()的调用次数大约在160万。

    究其原因,是因为我们在求解的过程时,重复了大量的计算过程, 在n = 8 的时候单单是f(3)就重复调用了8次。

    由上我们可以得出一个结论:在求解一个问题的同一实例时,在不同的递归中做重复性的工作,对资源的消耗可能是灾难性的。

    最后归纳一下要牢记的递归四条基本法则:

    1. 基准情形。必须总有某些基准情况,它无须递归就能求解,即递归必须有出口。
    2. 不断推进。对于那些需要递归求解的情形,每一次递归调用都必须要使求解状态朝基准情形的方向推进。
    3. 设计法则。假设所有的递归调用都能运行。
    4. 合成效益法则。在求解一个问题的同一实例时,切勿在不同的递归中做重复性的工作。

    1. 1、证明当n= 1时命题成立。2、假设n=m时命题成立,那么可以推导出在n=m+1时命题也成立。(m代表任意自然数)。3、归纳结论。 ↩︎

  • 相关阅读:
    BestCoder17 1001.Chessboard(hdu 5100) 解题报告
    codeforces 485A.Factory 解题报告
    codeforces 485B Valuable Resources 解题报告
    BestCoder16 1002.Revenge of LIS II(hdu 5087) 解题报告
    codeforces 374A Inna and Pink Pony 解题报告
    codeforces 483B Friends and Presents 解题报告
    BestCoder15 1002.Instruction(hdu 5083) 解题报告
    codeforces 483C.Diverse Permutation 解题报告
    codeforces 483A. Counterexample 解题报告
    NSArray中地内存管理 理解
  • 原文地址:https://www.cnblogs.com/chengjundu/p/11957429.html
Copyright © 2011-2022 走看看