精华内容
下载资源
问答
  • 以下对递归的描述正确的是
    2021-05-23 12:16:00

    The project team developing a new system is experienced in the domain. Although the new the new project is fairly large, it is not expected to vary much from applications that have been developed by t

    The project team developing a new system is experienced in the domain. Although the new project is fairly large, it is not expected to vary much from applications that have been developed by this team in the past. Which process model would be appropriate for this type of development?( )

    :A.prototyping

    B.waterfall

    C.V-model

    D.spiral

    更多相关内容
  • 递归是什么?关于递归的详细介绍

    千次阅读 2021-07-27 03:35:21
    递归一词还较常用于描述以自相似方法重复事物的过程。例如,当两面镜子相互之间近似平行时,镜中嵌套的图像是以无限递归的形式出现的。也可以理解为自我复制的过程。正式的定义在数学和计算机科学中,当一类对象或...

    递归,又译为递回,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。递归一词还较常用于描述以自相似方法重复事物的过程。例如,当两面镜子相互之间近似平行时,镜中嵌套的图像是以无限递归的形式出现的。也可以理解为自我复制的过程。

    9339e8139ebe4ed25453d4c9039208f4.png

    正式的定义

    34bb01078ab6ae94595c72596baacf91.png

    在数学和计算机科学中,当一类对象或方法可以由两个属性定义时,它们表现出递归行为:

    简单的基线条件---不使用递归产生答案的终止情况

    一组规则将所有其他情形缩减到基线条件

    例如,以下是某人祖先的递归定义:

    某人的父母是他的祖先(基线条件)

    某人祖先的祖先也是他的祖先(递归步骤)

    斐波那契数列是递归的经典例子:

    Fib(0) = 1 基线条件1;

    Fib(1) = 1 基线条件2;

    对所有整数n,n > 1时:Fib(n) = (Fib(n-1) + Fib(n-2))。

    许多数学公理基于递归规则。例如,皮亚诺公理对自然数的形式定义可以描述为:0是自然数,每个自然数都有一个后继数,它也是自然数。通过这种基线条件和递归规则,可以生成所有自然数的集合。

    递归定义的数学对象包括函数、集合,尤其是分形。

    递归还有多种开玩笑的“定义”。

    非正式定义

    f7800b5558877c1ddc9b754c88e9b516.png

    递归是当程序的一个步骤涉及调用程序本身的过程。经历递归的过程被称为“递归”。

    要理解递归,必须认识到程序和程序运行之间的区别。程序是基于一组规则的一组步骤。程序的运行实际上包括遵循规则和执行步骤。一个类比:一个程序就像一个书面的食谱;运行一个程序就像实际准备饭菜一样。

    递归与过程规范中对其他程序执行的引用相关,但不相同。例如,食谱可能指烹饪蔬菜,而需要依次加水等步骤是另一个程序。然而,递归过程是指(至少)其中一个步骤需要一个相同过程的新实例,就像酸面团配方需要上次制作相同配方时剩下的一些面团。这立即产生了一个无限循环的可能性;如果为了程序能够完成,在某些情况下跳过有问题的步骤,这样递归只能在定义中被正确使用,比如一个酸面团配方,它还告诉您如何获取一些生面团,以防您以前从未做过生面团。即使定义正确,递归过程对人类来说也不容易执行,因为它需要区分过程的新调用和旧调用(部分执行);这需要对程序的各种同步实例的进展程度进行一些管理。因此,递归定义在日常情况下非常罕见。一个例子可以是下面的寻找迷宫之路的过程,继续前进,直到到达出口或分支点(死角被认为是带有0个分支的分支点)。如果到达的点是出口,终止。否则,递归地使用该过程,依次尝试每个分支;如果每次试验都只到达死胡同而失败,返回到导致这个分支点的路径并报告失败。这是否真正定义了终止过程取决于迷宫的性质:它不允许循环。在任何情况下,执行该过程都需要仔细记录所有当前探索的分支点,以及哪些分支已经被彻底尝试过。

    在语言中

    语言学家诺姆·乔姆斯基和其他许多人都认为,一种语言中语法句子数量没有上限,语法句子长度也没有上限(超出了实际的限制,例如说出来的时间),这可以解释为自然语言中递归的结果。 这可以用句法范畴的递归定义来理解,例如句子,一个句子可以有一个结构,在这个结构中,跟在动词后面的是另一个句子:多萝西认为女巫是危险的,在这个结构中,女巫是危险一句出现在更大的句子中。因此,一个句子可以递归地(非常粗略地)定义为一个结构,包括一个名词短语、一个动词和可选的另一个句子。这实际上只是递归数学定义的一个特例。

    这提供了一种理解语言创造的方法——无限数量的语法句子——因为它立即预言句子可以是任意长度的:多萝西认为托托怀疑铁皮人说的....除了可以递归定义的句子之外,还有许多结构,因此一个句子可以通过许多方式将一个类别的实例嵌入另一个类别。多年来,一般来说,语言都被证明适合这种分析。

    然而,最近,丹尼尔·埃弗雷特基于他对皮拉汉语的主张,对递归是人类语言的一个基本属性这一普遍接受的观点提出了挑战。Andrew Nevins, David Pesetsky 和 Cilene Rodrigues是许多反对这一观点的人之一。 在任何情况下,文学自我引用都可以被认为不同于数学或逻辑递归。

    递归不仅在语法中起着至关重要的作用,在自然语言语义中也是如此。例如,单词and可以理解为一种功能,它可以应用于句子含义,从而创造出新的句子,同样也可以用于名词短语含义、动词短语含义和其它。它也适用于不及物动词、及物动词或双及物动词。为了给它提供一个适当灵活的单一表示,并且通常被定义为使得它可以将这些不同类型的含义中的任何一种作为参数。这可以通过为一个简单的案例定义它来实现,在这个案例中,它组合了句子,然后根据简单的案例递归地定义其他案例。

    递归语法是一种包含递归生成规则的形式语法。

    递归幽默

    递归有时在计算机科学、程序设计、哲学或数学教科书中幽默地使用,通常是通过给出循环定义或自我引用,在循环定义或自我引用中,假定的递归步骤不会更接近基线条件,而是导致无限回归。这样的书在词汇表中包含一个笑话条目并不罕见,大致如下:

    另一个笑话是“要理解递归,你必须理解递归。” 在谷歌网络搜索引擎的英文版本中,当搜索“递归”时,网站会提示“你的意思是:递归?”Andrew Plotkin的另一种形式如下:“如果你已经知道递归是什么,就记住答案。否则,找一个比你更靠近道Douglas Hofstadter 的人;然后问他或她什么是递归。”

    递归首字母缩写也可以是递归幽默的例子。例如,PHP代表“PHP Hypertext Preprocessor”,WINE代表“WINE Is Not an Emulator.”,GNU代表“GNU's not Unix”。

    在数学中

    1042bece889d94354068fb5db31d58a3.png递归定义的集合

    例子:自然数

    递归定义集合的典型例子由自然数给出:

    0是自然数;

    如果n是自然数,那么n+1也是自然数;

    自然数集合是满足前两个属性的最小集合。

    例子:真正可达命题的集合

    另一个有趣的例子是公理系统中所有“真正可达”命题的集合。

    如果一个命题是公理,它就是一个真正可达的命题。

    如果一个命题可以通过推理规则从真正可达命题中获得,那么它就是真正可达命题。

    真正可达命题集是满足这些条件的最小命题集。

    这个集合被称为“真正可达命题”,因为在数学基础的非建设性方法中,真正命题的集合可能大于由公理和推理规则递归构造的集合。有限细分规则

    有限细分规则是递归的几何形式,可用于创建类似分形的图像。细分规则从由有限多个标签标记的多边形集合开始,然后以仅依赖于原始多边形标签的方式将每个多边形细分为更小的标记多边形,这个过程可被迭代。创建康托集合的标准“中间三分之一”技术是细分规则,重心细分也是细分规则。函数递归

    一个函数可以根据自身被部分地定义。一个常见的例子是斐波那契数列: F(n) = F(n − 1) + F(n − 2)。为了使这样的定义有用,它必须引入非递归定义的值,在这种情况下,F(0) = 0,F(1) = 1。

    一个著名的递归函数是阿克曼函数,它不同于斐波那契数列,如果没有递归,它将无法被表达的。涉及递归定义的证明

    如前几节所述,将标准的案例证明技术应用于递归定义的集合或函数,会产生结构归纳法,这是数学归纳法的一种强有力的推广,广泛用于数学逻辑和计算机科学中的推导证明。递归优化

    动态规划是一种优化方法,它以递归的形式重述了多阶段或多步骤优化问题。动态规划的关键结果是贝尔曼方程(Bellman equation),它将优化问题早期(或较早步骤)的值写入到后期(或较晚步骤)的值中。递归定理

    在集合论中,这是一个保证递归定义函数存在的定理。给定一个集合X,集合X中的一个元素a和一个函数f: X -->X,定理表明存在一个唯一的函数F:N-->X(N表示包括0的自然数集合),使得满足F(0)=a , F(n+1)=f(F(n)) (对于任何自然数n)。

    唯一性的证明

    取两个函数F:N-->X和G:N-->X使得满足:

    F(0)=a

    G(0)=a

    F(n+1)=f(F(n))

    G(n+1)=f(G(n))

    其中a是集合X中的一个元素。

    数学归纳法可以证明对于所有自然数都有F(n)=G(n):

    基线条件:当n=0时,等式F(0)=a=G(0)成立;

    递归步骤:假设当k∈N时,F(k)=G(K)成立,然后F(k+1)=f(F(k))=f(G(k))=G(k+1),因此F(k)=G(k)意味着F(k+1)=G(k+1)

    通过归纳,F(n)=G(n) (其中n∈N)。

    在计算机科学中

    一种常见的简化方法是将一个问题分成相同类型的子问题。作为一种计算机编程技术,这被称为分治法,是许多重要算法设计的关键。分治法是一种自上而下解决问题的方法,通过解决越来越小的实例来解决问题。相反的方法是动态编程,这种方法是自下而上的方法,通过解决越来越大的实例来解决问题,直到达到所需的大小。

    递归的一个经典例子是阶乘函数的定义,这里用C代码给出:

    unsigned int factorial(unsigned int n) {

    if (n == 0) {

    return 1;

    } else {

    return n * factorial(n - 1);

    }

    }

    该函数在输入的较小版本(n-1)上递归调用自身,并将递归调用的结果乘以n,直到达到基线条件,类似于阶乘的数学定义。

    当一个函数被定义为更简单、通常更小的自身时,计算机编程中的递归就是一个例子。然后通过组合从问题的简单版本中获得的解决方案来设计问题的解决方案。递归的一个示例应用是在编程语言的解析器中。递归的最大优点是通过有限的计算机程序可以定义、解析或产生无限组可能的句子、设计或其他数据。

    递推关系是递归定义一个或多个序列的方程,可以“解决”某些特定类型的递推关系来获得非递归定义。

    在艺术领域

    6d315ab7f3bab391aa506f737bcedacf.png

    602a8fa8cee9c45b1d166fdf8985f06e.png

    俄罗斯娃娃或俄罗斯套娃是递归概念的一个物理艺术例子。

    自1320年乔托的Stefaneschi三联画问世以来,递归就一直用于绘画。它的中央面板包含红衣主教Stefaneschi的跪像,举着三联画本身作为祭品。

    M.C. Eschers 印刷画廊 (1956)描绘了一个扭曲的城市,其中包含一个递归包含图片的画廊,因此无限。

    展开全文
  • 以下叙述正确的是分值:2A. C语言中各函数之间既允许直接递归调用也允许间接递归调用B. C语言中各函数之间既不允许直接递归调用也不允许间接递归调用C. C语言中各函数之间既允许直接递归调用不允许间接递归调用D. ...

    线作业试卷列表

    单选

    1.以下叙述正确的是

    分值:2

    A. C语言中各函数之间既允许直接递归调用也允许间接递归调用

    B. C语言中各函数之间既不允许直接递归调用也不允许间接递归调用

    C. C语言中各函数之间既允许直接递归调用不允许间接递归调用

    D. C语言中各函数之间既不允许直接递归调用允许间接递归调用

    2.以下程序的输出结果是

    char str[ ]="ABCD",*p=str;

    printf ("%d ",*(p+4)); 分值:2

    A. 68

    B. 0

    C. 字符D的地址

    D. 不能确定的值

    3.以下程序的输出结果是

    main( )

    { int a[ ]={1,2,3,4 },i,x=0;

    for(i=0; i<4; i++) { sub(a,&x); printf(“%d”, x); }

    printf(“ ”);

    }

    sub( int *s, int *y)

    { static int t=3;

    *y=s[t]; t-- ; } 分值:2

    A. 1 2 3 4   B. 4 3 2 1

    C. 0 0 0 0   D. 4 4 4 4

    4.以下程序的输出结果是

    main( )

    { int k=4, m=1,p;

    p=func(k,m);

    printf(“%d,”,p);

    p=func(k,m);

    printf(“%d ”,p);

    }

    func( int a, int b);

    { static int m, i=2;

    i+=m+1;

    m=i+a+b;

    return(m);

    } 分值:2

    A. 8,17   B. 8,16

    C. 8,20   D. 8,8

    5.以下程序的输出结果是

    void fun(int *s)

    { static int j=0;

    do

    s[j]+=s[j+1];

    while(++j<2);

    }

    main( )

    { int k,a[10]={1,2,3,4,5};

    for (k=1; k<3; k++) fun(a);

    for (k=0; k<5; k++) printf(“%d”,a[k] );

    } 分值:2

    A. 34756

    B. 23445

    C. 35745

    D. 12345

    6.以下程序的输出结果是

    f(int a)

    { int b=0;

    static int c=3;

    a=c ++, b ++;

    return( a );

    }

    main( )

    { int a=2,i,k;

    for(i=0 ; i<2; i++) k=f(a++);

    printf(“%d ”,k);

    } 分值:2

    A. 3

    B. 6

    C. 5

    D. 4

    7.以下程序的输出结果是

    int m=13;

    int fun2(int x, int y)

    { int m=3;

    return(x * y C m);

    }

    main( )

    { int a=7,b=5;

    printf(“%d ”,fun2(a,b)/m ); } 分值:2

    A. 1   B. 2

    C. 7   D. 10

    8.C语言中, 形参的缺省的存储类说明是

    分值:2

    A. auto ( 自动 )

    B. static ( 静态 )

    C. register ( 寄存器 )

    D. extern ( 外部 )

    9.以下选项中正确的整型常量是 __________。

    分值:2

    A. 12.

    B. -20

    C. 1,000

    D. 4 5 6

    10.以下选项中正确的实型常量是 __________。

    分值:2

    A. 0

    B. 3.1415

    C. 0.329*102

    D. .871

    11.以下选项中不正确的实型常量是__________。

    分值:2

    A. 2.607E-1   B. 0.8103e 2

    C. -77.77   D. 456e-2

    12.以下选项中不合法的用户标识符是_________。

    分值:2

    A. abc.c   B. file

    C. Main   D. PRINTF

    13.以下选项中不合法的用户标识符是__________。

    分值:2

    A. _123   B. printf

    C. A$   D. Dim

    14.C语言中运算对象必需是整型的运算符是__________。

    分值:2

    A. %   B. /

    C. !   D. **

    15.可在C程序中用作用户标识符的一组标识符是_________。

    分值:2

    A. void define WORD

    B. as_b3 _123 If

    C. For -abc case

    D. 2c DO SIG

    16.若变量已正确定义并赋值,符合C语言语法的表达式是_________。

    分值:2

    A. a=a+7;

    B. a=7+b+c,a++

    C. int(12.3%4)

    D. a=a+7=c+b

    17.以下叙述中正确的是_________。

    分值:2

    A. a是实型变量,C允许进行以下赋值a=10,因此可以这样说:实型变量允许赋值整型值。

    B. 在赋值表达式中,赋值号左边既可以是变量也可以是任意表达式。

    C. 执行表达式a=b后,在内存中a 和 b存储单元中的原有值都将被改变,a的值已由原值改变为b 的值, b 的值由原值变为0。

    D. 已有a=3,b=5。当执行了表达式 a=b ,b=a 之后,已使a 中的值为5,b 中的值为3。

    18.以下叙述中正确的是________。

    分值:2

    A. 在C程序中无论整数还是实数,只要在允许的范围内都能准确无误的表示。

    B. C程序由主函数组成。

    C. C程序由函数组成。

    D. C程序由函数和过程组成。

    19.若a、b、c、d、都是int类型变量且初值为0,以下选项中不正确的赋值语句是_________。

    分值:2

    A. a=b=c=d=100;   B. d++;

    C. c+b;   D. d=(c=22)-(b++);

    20.以下合法的C语言赋值语句是_________。

    分值:2

    A. a=b=58   B. k=int(a+b);

    C. a=58,b=58   D. --i;

    21.若变量已正确说明为int类型,要给 分值:2

    A. read(a,b,c);

    B. scanf(“ %d%d%d” ,a,b,c);

    C. scanf(“ %D%D%D” ,&a,%b,%c);

    D. scanf(“ %d%d%d”,&a,&b,&c);

    22.若变量已正确定义,要将a和b中的数进行交换,下面不正确的语句组是_________。

    分值:2

    A. a=a+b, b=a-b, a=a-b;

    B. t=a, a=b, b=t;

    C. a=t; t=b; b=a;

    D. t=b; b=a; a=t;

    23.若有以下程序段,c3中的值是__________。

    int c1=1,c2=2,c3;

    c3=c1/c2; 分值:2

    A. 0   B. 1/2

    C. 0.5   D. 1

    24.若有以下程序段 ,其输出结果是__________。

    int a=0,b=0,c=0;

    c=(a-=a-5),(a=b,b+3);

    printf(“ %d,%d,%d ”,a,b,c); 分值:2

    A. 0,0,-10   B. 0,0,5

    C. -10,3,-10   D. 3,0,-10

    25.当运行以下程序时,在键盘上从第一列开始输入9876543210(此处代表Enter),则程序的输出结果是__________。

    main( )

    { int a; float b,c;

    scanf(“ %2d%3f%4f”,&a,&b,&c);

    printf(“ a=%d,b=%f,c=%f ”,a,b,c);}  分值:2

    A. a=98,b=765,c=4321

    B. a=10,b=432,c=8765

    C. a=98,b=765.000000,c=4321.000000

    D. a=98,b=765.0,c=4321.0

    26.若int类型占两个字节,则以下程序段的输出是__________。

    int a=-1;

    printf(“ %d,%u ”,a,a);  分值:2

    A. -1,-1   B. -1,32767

    C. -1,32768   D. -1,65535

    27.以下程序段的输出是__________。

    float a=3.1415;

    Printf(“ |%6.0f| ”,a);  分值:2

    A. |3.1415|   B. | 3.0|

    C. | 3|   D. | 3.|

    28.以下程序段的输出是__________。

    float a=57.666;

    pirntf(“ %010.2f ”,a);  分值:2

    A. *0000057.66*

    B. * 57.66*

    C. *0000057.67*

    D. * 57.67*

    29.C语言中的简单类型有

    分值:2

    A. 整型,实型,逻辑型

    B. 整型,实型,字符型

    C. 整型,字符型,逻辑型

    D. 整型,实型,逻辑型,字符型

    30.C语言中,字符型(char)数据在微机内存中的存储形式是

    分值:2

    A. 反码   B. 补码

    C. EBCDIC码   D. ASCII码

    31.C语言中不合法的字符常量是

    分值:2

    A. ′\0XFF′   B. ‘\65′

    C. ′&′   D. ′\028′

    32.C语言中不合法的字符串常量是

    分值:2

    A. "121"   B. ′Y=′

    C. " "   D. "ABCD\X6d"

    33.判断char型变量C是否为大写字母的最简单且正确的表达式是

    分值:2

    A. ‘A ’<=C=‘Z’

    B. (C>=′A′)&(C<=′Z′)

    C. (′A′<=C)AND(′Z′>=C)

    D. (C>=′A′)&&(C<=′Z′)

    34.以下程序的输出结果是

    main( )

    { char c1=′a′,c2=′y′;

    printf("%d,%d ",c1,c2);

    } 分值:2

    A. 因输出格式不合法,无正确输出

    B. 65,90

    C. A,Y

    D. 65,89

    35.以下程序的输出结果是

    main( )

    {char x=′a′

    x=(x>=′A′&& x<=′Z′)?(x+32):x;

    printf("%c ",x);

    } 分值:2

    A. A   B. a

    C. Z   D. z

    36.以下各组选项中,均能正确定义二维实型数组a的选项是__________。

    分值:2

    A. float a[3][4];

    float a[][4];

    float a[3][]={{1},{0}};

    B. float a(3,4);

    float a[3][4];

    float a[][]={{0},{0}};

    C. float a[3][4];

    static float a[][4]={{0},{0}};

    auto float a[][4]={{0},{0},{0}};

    D. float a[3][4];

    float a[3][];

    float a[][4];

    37.以下正确的说法是__________。

    分值:2

    A. 实参和与其对应的形参占用独立的存储单元

    B. 实参和与其对应的形参共占用一个存储单元

    C. 只有当实参和与其对应的形参同名时才共占用一个存储单元

    D. 形参是虚拟的,不占用存储单元

    38.以下说法中正确的是

    分值:2

    A. C语言程序总是从第一个定义的函数开始执行

    B. 在C语言程序中,要调用的函数必须在main函数中定义

    C. C语言程序总是从main函数开始执行

    D. C语言程序中的main函数必须放在程序的开始部分

    39.以下函数的类型是

    fff(float x){

    printf("%d ",x*x);

    } 分值:2

    A. 与参数x的类型相同   B. void类型

    C. int类型   D. 无法确定

    40.以下程序的输出结果是

    func(int a,int b)

    { int c

    c=a+b;

    return c; }

    main( )

    { int x=6,y=7,z=8,r;

    r=func((x--,y++,x+y),z--);

    printf("%d ",r); } 分值:2

    A. 11   B. 20

    C. 21   D. 31

    41.以下程序有错,错误原因是__________。

    main()

    {int *p,i;char *q,ch;

    p=&i;

    q=&ch;

    *p=40;

    *p=*q;

    }

    分值:2

    A. p和q类型不一致,不能执行*p=*q;语句

    B. *p中存放的是地址值,因此不能执行*p=40;语句

    C. q没有指向具体的存储单元,所以*q没有实际意义

    D. q虽然指向了具体的存储单元,但该单元中没有确定的值,所以不能执行*p=*q;语句

    42.以下程序的输出结果是

    double f(int n)

    { int i; double s;

    s=1.0;

    for(i=1; i<=n; i++) s+=1.0/i;

    return s;

    }

    main()

    { int i,m=3; float a=0.0;

    for(i=0; i

    printf("%f ",a)L;

    }  分值:2

    A. 5.500000   B. 3.000000

    C. 4.000000   D. 8.25

    43.若有定义: int x,*pb;则在以下正确的赋值表达式是

    分值:2

    A. pb=&x   B. pb=x

    C. *pb=&x   D. *pb=*x

    44.以下程序的输出结果是

    #include "stdio.h"

    main()

    { printf("%d ",NULL); } 分值:2

    A. 因变量无定义输出不定值

    B. 0

    C. -1

    D. 1

    45.有如下语句int a=10,b=20;*p1=&a,*p2=&b;如果让两个指针变量均指向b,正确的赋值方式是__________。

    分值:2

    A. *p1=*p2;   B. p1=p2;

    C. p1=*p2;   D. *p1=*p2;

    46.已知指针P的指向如图所示,则表达式*P++的值是

    a[0] a[1] a[2] a[3] a[4]

    10   20   30   40   50

    P 分值:2

    A. 20   B. 30

    C. 21   D. 31

    47.已知指针P的指向如图所示,则表达式* ++ P的值是

    a[0] a[1] a[2] a[3] a[4]

    10   20   30   40   50

    P 分值:2

    A. 20   B. 30

    C. 21   D. 31

    48.已知指针P的指向如图所示,则表达式++*P的值

    a[0] a[1] a[2] a[3] a[4]

    10   20   30   40   50

    P 分值:2

    A. 20   B. 30

    C. 21   D. 31

    49.以下能正确进行字符串赋值、赋初值的语句组是

    分值:2

    A. char s[5]={′a′,′e′,′i′,′o′,′u′};

    B. char *s; s="good!";

    C. char s[5]="good!";

    D. char s[5]; s="good";

    50.若有以下说明和定义,则对fun函数的正确调用语句是

    分值:2

    A. a=fun; a(w);

    B. a=fun; (*a)(&c);

    C. b=fun; *b(w);   D. fun(b);

    main( )

    {

    int (*a)(int*),*b( ),w[10],c;

    :

    :

    }

    fun(int *c) {...}

    展开全文
  • 递归函数的正确思维方法

    万次阅读 多人点赞 2018-02-01 20:40:59
    什么是递归 简单的定义: “当函数直接或者间接调用自己时,则发生了递归.” 说起来简单, 但是理解起来复杂, 因为递归并不直观, 也不符合我们的思维习惯, 相对于递归, 我们更加容易理解迭代. 因为我们日常生活中...


    什么是递归

    简单的定义: “当函数直接或者间接调用自己时,则发生了递归.” 说起来简单, 但是理解起来复杂, 因为递归并不直观, 也不符合我们的思维习惯, 相对于递归, 我们更加容易理解迭代. 因为我们日常生活中的思维方式就是一步接一步的, 并且能够理解一件事情做了N遍这个概念. 而我们日常生活中几乎不会有递归思维的出现.
    举个简单的例子, 即在C/C++中计算一个字符串的长度. 下面是传统的方式, 我们一般都这样通过迭代来计算长度, 也很好理解.

    size_t length(const char *str) {
      size_t length = 0;
      while (*str != 0) {
        ++length;
        ++str;
      }
    
      return length;
    }
    

    而事实上, 我们也可以通过递归来完成这样的任务.

    size_t length(const char *str) {
      if (*str == 0) {
        return 0;
      }
      return length(++str) + 1;
    }
    

    只不过, 我们都不这么做罢了, 虽然这样的实现有的时候可能代码更短, 但是很明显, 从思维上来说更加难以理解一些. 当然, 我是说假如你不是习惯于函数式语言的话. 这个例子相对简单, 稍微看一下还是能明白吧.
    迭代的算法可以这样描述: 从第一个字符开始判断字符串的每一个字符, 当该字符不为0的时候, 该字符串的长度加一.
    递归的算法可以这样描述: 当前字符串的长度等于当前字符串除了首字符后, 剩下的字符串长度+1.
    作为这么简单的例子, 两种算法其实大同小异, 虽然我们习惯迭代, 但是, 也能看到, 递归的算法无论是从描述上还是实际实现上, 并不比迭代要麻烦.

    理解递归

    在初学递归的时候, 看到一个递归实现, 我们总是难免陷入不停的回溯验证之中, 因为回溯就像反过来思考迭代, 这是我们习惯的思维方式, 但是实际上递归不需要这样来验证. 比如, 另外一个常见的例子是阶乘的计算. 阶乘的定义: “一个正整数的阶乘(英语:factorial)是所有小于或等于该数的正整数的积,并且0的阶乘为1。” 以下是Ruby的实现:

    def factorial(n) 
      if n <= 1 then
        return 1
      else
        return n * factorial(n - 1)
      end
    end
    

    我们怎么判断这个阶乘的递归计算是否是正确的呢? 先别说测试, 我说我们读代码的时候怎么判断呢?
    回溯的思考方式是这么验证的, 比如当n = 4时, 那么factoria(4)等于4 * factoria(3), 而factoria(3)等于3 * factoria(2)factoria(2)等于2 * factoria(1), 等于2 * 1, 所以factoria(4)等于4 * 3 * 2 * 1. 这个结果正好等于阶乘4的迭代定义.
    用回溯的方式思考虽然可以验证当n = 某个较小数值是否正确, 但是其实无益于理解.
    Paul Graham提到一种方法, 给我很大启发, 该方法如下:

    1. 当n=0, 1的时候, 结果正确.
    2. 假设函数对于n是正确的, 函数对n+1结果也正确.
      如果这两点是成立的,我们知道这个函数对于所有可能的n都是正确的。

    这种方法很像数学归纳法, 也是递归正确的思考方式, 事实上, 阶乘的递归表达方式就是1!=1,n!=(n-1)!×n(见wiki). 当程序实现符合算法描述的时候, 程序自然对了, 假如还不对, 那是算法本身错了…… 相对来说, n,n+1的情况为通用情况, 虽然比较复杂, 但是还能理解, 最重要的, 也是最容易被新手忽略的问题在于第1点, 也就是基本用例(base case)要对. 比如, 上例中, 我们去掉if n <= 1的判断后, 代码会进入死循环, 永远不会结束.

    使用递归

    既然递归比迭代要难以理解, 为啥我们还需要递归呢? 从上面的例子来看, 自然意义不大, 但是很多东西的确用递归思维会更加简单……
    经典的例子就是斐波那契数列, 在数学上, 斐波那契数列就是用递归来定义的:

    ·F0 = 0
    ·F1 = 1 
    ·Fn = Fn – 1 + Fn – 2

    有了递归的算法, 用程序实现实在再简单不过了:

    def fibonacci(n)
      if n == 0 then
        return 0
      elsif n == 1 then
        return 1
      else
        return fibonacci(n - 1) + fibonacci(n - 2)
      end
    end
    

    改为用迭代实现呢? 你可以试试.
    上面讲了怎么理解递归是正确的, 同时可以看到在有递归算法描述后, 其实程序很容易写, 那么最关键的问题就是, 我们怎么找到一个问题的递归算法呢?
    Paul Graham提到, 你只需要做两件事情:

    1. 你必须要示范如何解决问题的一般情况, 通过将问题切分成有限小并更小的子问题.
    2. 你必须要示范如何通过有限的步骤, 来解决最小的问题(基本用例).
      如果这两件事完成了, 那问题就解决了. 因为递归每次都将问题变得更小, 而一个有限的问题终究会被解决的, 而最小的问题仅需几个有限的步骤就能解决.

    这个过程还是数学归纳法的方法, 只不过和上面提到的一个是验证, 一个是证明.
    现在我们用这个方法来寻找汉诺塔这个游戏的解决方法.(这其实是数学家发明的游戏)

    有三根杆子A,B,C。A杆上有N个(N>1)穿孔圆盘,盘的尺寸由下到上依次变小。要求按下列规则将所有圆盘移至C杆:
    1.每次只能移动一个圆盘.
    2.大盘不能叠在小盘上面.

    汉诺塔

    这个游戏在只有3个盘的时候玩起来较为简单, 盘越多, 就越难, 玩进去后, 你就会进入一种不停的通过回溯来推导下一步该干什么的状态, 这是比较难的. 我记得第一次碰到这个游戏好像是在大航海时代某一代游戏里面, 当时就觉得挺有意思的. 推荐大家都实际的玩一下这个游戏, 试试你脑袋能想清楚几个盘的情况.
    现在我们来应用Paul Graham的方法思考这个游戏.

    一般情况:
    当有N个圆盘在A上, 我们已经找到办法将其移到C杠上了, 我们怎么移动N+1个圆盘到C杠上呢? 很简单, 我们首先用将N个圆盘移动到C上的方法将N个圆盘都移动到B上, 然后再把第N+1个圆盘(最后一个)移动到C上, 再用同样的方法将在B杠上的N个圆盘移动到C上. 问题解决.

    基本用例:
    当有1个圆盘在A上, 我们直接把圆盘移动到C上即可.

    算法描述大概就是上面这样了, 其实也可以看作思维的过程, 相对来说还是比较自然的. 下面是Ruby解:

    def hanoi(n, from, to, other)
      if n == 1 then
        puts from + ' -> ' + to
      else
        hanoi(n-1, from, other, to)
        hanoi(1, from, to, other)
        hanoi(n-1, other, to, from)
      end
    end
    

    当n=3时的输出:

    A -> C
    A -> B
    C -> B
    A -> C
    B -> A
    B -> C
    A -> C

    上述代码中, from, to, other的作用其实也就是提供一个杆子的替代符, 在n=1时, 其实也就相当于直接移动. 看起来这么复杂的问题, 其实用递归这么容易, 没有想到吧. 要是想用迭代来解决这个问题呢? 还是你自己试试吧, 你试的越多, 就能越体会到递归的好处.

    递归的问题

    当然, 这个世界上没有啥时万能的, 递归也不例外, 首先递归并不一定适用所有情况, 很多情况用迭代远远比用递归好了解, 其次, 相对来说, 递归的效率往往要低于迭代的实现, 同时, 内存好用也会更大, 虽然这个时候可以用 尾递归 来优化, 但是尾递归并不是一定能简单做到.

    展开全文
  • 什么是递归递归,就是函数在运行的过程中调用自己。代码示例defrecursion(n):print(n)recursion(n+1)recursion(1)出现的效果就是,这个函数在不断的调用自己,每次调用就n+1,相当于循环了。可是为何执行了900多次...
  • 因为它不但能考察一个程序员的算法功底,还能很好的考察时间空间复杂度的理解和分析。 本文只讲一题,也是几乎所有算法书讲递归的第一题,但力争讲出花来,在这里分享四点不一样的角度,让你有不同的收获。 ...
  • 递归算法(对递归的理解,解题思路,适合新手参考)目录前言递归什么是递归怎么使用递归为什么用递归案例:青蛙爬楼梯更好的解法总结 目录 前言 本人是一个刚刚开始学习算法的新手,在我学习算法的过程中,总是...
  • 前言 递归,是一个非常重要的概念,也是面试中非常喜欢考的。因为它不但能考察一个程序员的算法功底,还能很好的考察时间空间复杂度的理解和分析。本文只讲一题,也是几乎所有算法书讲递归的第一题...
  • 前言 递归,是一个非常重要的概念,也是面试中非常喜欢考的。因为它不但能考察一个程序员的算法功底,还能很好...大家都知道,一个方法自己调用自己就是递归,没错,但这只是对递归最表层的理解。 那么递归的实质...
  • 对于从命令式语言开始学习编程的程序员天生对此有理解缺陷, 而对于从类似C++这种函数式编程范式不友好的语言开始学习编程的程序员就更加如此了.(比如我自己) 碰巧(其实不巧)最近在读这本书(这本书国内没有引进, ...
  • 【单选题】以下关于函数参数和返回值的描述,正确的是:【单选题】下面代码的输出结果是‪‬‪‬‪‬‪‬‪‬‮‬‭‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬...
  • 因为它不但能考察一个程序员的算法功底,还能很好的考察时间空间复杂度的理解和分析。 本文只讲一题,也是几乎所有算法书讲递归的第一题,但力争讲出花来,在这里分享四点不一样的角度,让你有不同的收获。 时空...
  • 算法 分治策略与递归

    2022-02-14 00:42:45
    通过循环修改为递归来分析分治策略,以及分析快排的递归实现与非递归实现
  • 递归正确切入方法

    2017-07-22 22:44:00
    一、递归的定义 定义: 当函数直接或者间接调用自己时,则发生了递归。 二、理解递归 先举个递归在阶乘中的例子:阶乘的定义: “一个正整数的阶乘是所有小于或等于该数的正整数的积,并且0的阶乘为1。” 以下...
  • 递归

    2018-07-11 10:10:30
    一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算...
  • 递归函数简单的理解就是在不停地调用自己,如果不懂请参照“讲解”。其实递归函数在使用时只需要明确一点:我写这个函数是干嘛用的,比如在用递归写计算N!的C语言程序时,只需要明确我写的函数是用来计算当前传入...
  • 栈与递归

    2021-04-11 11:54:16
    递归是算法设计中最常用的手段,它通常将一个大型复杂问题的描述和求解变得简洁和清晰。因此递归算法常常比非递归算法更容易设计,尤其是当问题本身或所涉及的数据结构是递归定义的时候,使用递归方法更加合适。 ...
  • 递归思想的一点思考参考,还有递归的实现,和消除递归的思路和实现代码
  • 掌握递归下降语法分析程序的设计思想,加深对递归下降语法分析程序的理解。 通过设计编制调试具体的YACC程序,掌握YACC源程序的基本组成。 实验内容: 1. PL/0编译程序进行裁减和改造,使其仅包含词法和语法分析...
  • 该楼层疑似违规已被系统折叠隐藏此楼查看此楼西工大机考《C语言程序设计》网考寻求答案(非免费)找我Q...,以下选项中a数组元素正确引用的是( )。A.a[2][!1]B.a[2][3]C.a[0][3]D.a[1>2][!1]2. 以下描述错误的是...
  • 递归案例python

    2020-11-23 14:44:14
    而对应的中文翻译 ”递归“ 却表达了两个意思:”递“+”归“。 这两个意思,正是递归思想的精华所在。 从这层次上来看,中文翻译反而更达意。 递归是静中有动,有去有回。 循环是动静如一,有去无回。 python递归...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 71,676
精华内容 28,670
关键字:

以下对递归的描述正确的是