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、归纳结论。 ↩︎

  • 相关阅读:
    14-3 SQL Server基本操作
    14-2 SQL语言简介
    14-1数据库基础--数据库相关技术
    2.9_Database Interface ADO结构组成及连接方式实例
    2.8_Database Interface ADO由来
    2.7_Database Interface OLE-DB诞生
    容器化技术之K8S
    容器化技术之Docker
    NLP(二)
    cmake
  • 原文地址:https://www.cnblogs.com/chengjundu/p/11957429.html
Copyright © 2011-2022 走看看