`
近乎sns
  • 浏览: 12419 次
  • 性别: Icon_minigender_2
  • 来自: 青岛
文章分类
社区版块
存档分类
最新评论

递归与尾递归(C语言)

C++ 
阅读更多

在计算机科学领域中,递归式通过递归函数来实现的。程序调用自身的编程技巧称为递归( recursion)。

一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解, 递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。

一般来说,递归需要有:边界条件、递归前进段和递归返回段。

当边界条件不满足时,递归前进;当边界条件满足时,递归返回。

注意:

(1) 递归就是在过程或函数里调用自身;

(2) 在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口。

基本递归

问题:计算n!

数学上的计算公式为:n!=n×(n-1)×(n-2)……2×1

使用递归的方式,可以定义为:

以递归的方式计算4!

F(4)=4×F(3)            递归阶段

    F(3)=3×F(2)

         F(2)=2×F(1)

              F(1)=1  终止条件

         F(2)=(2)×(1)    回归阶段

    F(3)=(3)×(2)

F(4)=(4)×(6)

24                  递归完成

以递归方式实现阶乘函数的实现:

int fact(int n) {    if(n < 0)        return 0;    else if (n == 0 || n == 1)        return 1;    else
        return n * fact(n - 1);
}

下面来详细分析递归的工作原理

先看看C语言中函数的执行方式,需要了解一些关于C程序在内存中的组织方式:

BSS段:(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。

数据段 :数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。 

代码段: 代码段(code segment/text segment)通常是指用来存放 程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读 , 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量 ,例如字符串常量等。程序段为程序代码在内存中的映射.一个 程序可以在内存中多有个副本.

堆(heap) :堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc/free等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张)/释放的内存从堆中被剔除(堆被缩减)

栈(stack) :栈又称堆栈, 存放程序的局部变量(但不包括static声明的变量, static 意味着 在数据段中存放变量)。除此以外,在函数被调用时,栈用来传递参数和返回 值。由于栈的后进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

堆的增长方向为从低地址到高地址向上增长,而栈的增长方向刚好相反(实际情况与CPU的体系结构有关)

当C程序中调用了一个函数时,栈中会分配一块空间来保存与这个调用相关的信息,每一个调用都被当作是活跃的。栈上的那块存储空间称为活跃记录或者栈帧

栈帧由5个区域组成:输入参数、返回值空间、计算表达式时用到的临时存储空间、函数调用时保存的状态信息以及输出参数,参见下图:

可以使用下面的程序来检验:

#include <stdio.h>int g1=0, g2=0, g3=0;int max(int i)
{    int m1 = 0, m2, m3 = 0, *p_max;    static n1_max = 0, n2_max, n3_max = 0;
    p_max = (int*)malloc(10);
    printf("打印max程序地址\n");
    printf("in max: 0x%08x\n\n",max);
    printf("打印max传入参数地址\n");
    printf("in max: 0x%08x\n\n",&i);
    printf("打印max函数中静态变量地址\n");
    printf("0x%08x\n",&n1_max); //打印各本地变量的内存地址
    printf("0x%08x\n",&n2_max);
    printf("0x%08x\n\n",&n3_max);
    printf("打印max函数中局部变量地址\n");
    printf("0x%08x\n",&m1); //打印各本地变量的内存地址
    printf("0x%08x\n",&m2);
    printf("0x%08x\n\n",&m3);
    printf("打印max函数中malloc分配地址\n");
    printf("0x%08x\n\n",p_max); //打印各本地变量的内存地址
    if(i) return 1;    else return 0;
}int main(int argc, char **argv)
{    static int s1=0, s2, s3=0;    int v1=0, v2, v3=0;    int *p;    
    p = (int*)malloc(10);
    printf("打印各全局变量(已初始化)的内存地址\n");
    printf("0x%08x\n",&g1); //打印各全局变量的内存地址
    printf("0x%08x\n",&g2);
    printf("0x%08x\n\n",&g3);
    printf("======================\n");
    printf("打印程序初始程序main地址\n");
    printf("main: 0x%08x\n\n", main);
    printf("打印主参地址\n");
    printf("argv: 0x%08x\n\n",argv);
    printf("打印各静态变量的内存地址\n");
    printf("0x%08x\n",&s1); //打印各静态变量的内存地址
    printf("0x%08x\n",&s2);
    printf("0x%08x\n\n",&s3);
    printf("打印各局部变量的内存地址\n");
    printf("0x%08x\n",&v1); //打印各本地变量的内存地址
    printf("0x%08x\n",&v2);
    printf("0x%08x\n\n",&v3);
    printf("打印malloc分配的堆地址\n");
    printf("malloc: 0x%08x\n\n",p);
    printf("======================\n");
    max(v1);
    printf("======================\n");
    printf("打印子函数起始地址\n");
    printf("max: 0x%08x\n\n",max);    return 0;
}

栈是用来存储函数调用信息的绝好方案,然而栈也有一些缺点:

栈维护了每个函数调用的信息直到函数返回后才释放,这需要占用相当大的空间,尤其是在程序中使用了许多的递归调用的情况下。除此之外,因为有大量的 信息需要保存和恢复,因此生成和销毁活跃记录需要消耗一定的时间。我们需要考虑采用迭代的方案。幸运的是我们可以采用一种称为尾递归的特殊递归方式来避免 前面提到的这些缺点。

尾递归

定义

如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。

原理

当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内 最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个, 这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。虽然编译器能够优化尾递归造成的栈溢出问题,但是在编程中,我们还是应该尽量避免尾递 归的出现,因为所有的尾递归都是可以用简单的goto循环替代的。

实例

为了理解尾递归是如何工作的,让我们再次以递归的形式计算阶乘。首先,这可以很容易让我们理解为什么之前所定义的递归不 是尾递归。回忆之前对计算n!的定义:在每个活跃期计算n倍的(n-1)!的值,让n=n-1并持续这个过程直到n=1为止。这种定义不是尾递归的,因为 每个活跃期的返回值都依赖于用n乘以下一个活跃期的返回值,因此每次调用产生的栈帧将不得不保存在栈上直到下一个子调用的返回值确定。现在让我们考虑以尾 递归的形式来定义计算n!的过程。

这种定义还需要接受第二个参数a,除此之外并没有太大区别。a(初始化为1)维护递归层次的深度。这就让我们避免了每次还需要将返回值再乘以n。然而,在每次递归调用中,令a=na并且n=n-1。继续递归调用,直到n=1,这满足结束条件,此时直接返回a即可。

代码实例给出了一个C函数facttail,它接受一个整数n并以尾递归的形式计算n!。这个函数还接受一个参数a,a的初始值为1。facttail使用a来维护递归层次的深度,除此之外它和fact很相似。读者可以注意一下函数的具体实现和尾递归定义的相似之处。

int facttail(int n, int a)
{    if (n < 0)        return 0;    else if (n == 0)        return 1;    else if (n == 1)        return a;    else
        return facttail(n - 1, n * a);
}

示例中的函数是尾递归的,因为对facttail的单次递归调用是函数返回前最后执行的一条语句。在facttail中 碰巧最后一条语句也是对facttail的调用,但这并不是必需的。换句话说,在递归调用之后还可以有其他的语句执行,只是它们只能在递归调用没有执行时 才可以执行。

尾递归是极其重要的,不用尾递归,函数的堆栈耗用难以估量,需要保存很多中间函数的堆栈。比如f(n, sum) = f(n-1) + value(n) + sum; 会保存n个函数调用堆栈,而使用尾递归f(n, sum) = f(n-1, sum+value(n)); 这样则只保留后一个函数堆栈即可,之前的可优化删去。

也许在C语言中有很多的特例,但编程语言不只有C语言,在函数式语言Erlang中(亦是栈语言),如果想要保持语言的高并发特性,就必须用尾递归来替代传统的递归。

文章来之近乎sns开发分享社区

分享到:
评论

相关推荐

    C语言递归函数的学习与运用

    然而,标准C语言并不保证尾递归优化。 总之,理解并熟练掌握C语言中的递归函数,可以极大地提升编程能力,解决复杂问题。但在实际应用中,应谨慎考虑递归的效率和适用性,根据具体情况选择合适的方法。

    C语言递归调戏谷歌地球.rar

    同时,通过尾递归优化(如果编译器支持)或使用迭代方法,可以提高递归函数的效率。 总结,"C语言递归调戏谷歌地球"这一主题结合了基础的编程概念与实际的GIS应用。理解和掌握递归是提升C语言编程能力的关键步骤,...

    C语言递归函数教学的设计与探讨.pdf

    为了避免这一问题,有时候需要考虑使用迭代代替递归,或者使用尾递归优化等技术。同时,教师也应当鼓励学生去思考递归算法的时间复杂度和空间复杂度,使他们能够对递归有更全面的认识。 在实际应用中,递归函数常...

    有关C语言的递归算法

    - **尾递归**:在函数返回的时候,调用自身,并且return语句不能包含表达式。这种情况下,编译器或解释器可以进行优化,使递归调用的开销尽可能小。 3. **常见的递归问题** - **阶乘计算**:递归地计算一个数的...

    C语言中递归的探讨.pdf

    在尾递归中,由于每次递归调用后没有其他语句执行,编译器可以优化递归过程,避免创建新的堆栈帧,从而减少内存消耗。在某些情况下,尾递归可以被转换为循环,以便提高效率。 递归函数必须有一个明确的终止条件,以...

    C语言中的递归函数及其注意

    7. **尾递归**:在某些编译器或解释器支持的情况下,可以使用尾递归优化。尾递归是指递归调用是函数体的最后一个操作,且返回值不依赖于递归调用的结果。这种情况下,编译器可能能够优化掉不必要的栈空间分配,降低...

    尾递归优化:C语言中的性能艺术

    C语言是一种通用的编程语言,由丹尼斯·里奇(Dennis Ritchie)在20世纪70年代早期于美国电话电报公司(AT&T)的贝尔实验室开发。C语言以其高效性、灵活性和可移植性而闻名,它是一种过程式编程语言,提供了对底层...

    基于C语言的递归程序设计分析.pdf

    为避免这种情况,可以考虑使用尾递归优化或者改写为迭代方式。 3. 递归的应用场景 - 数学问题:如计算阶乘、斐波那契数列等。 - 数据结构:在处理树和图等数据结构时,如遍历二叉树、深度优先搜索(DFS)等。 - ...

    递归在C语言中的实现.pdf

    此外,在一些特定情况下,如斐波那契数列的计算,可以使用“尾递归”来优化递归调用,减少栈空间的使用。 总之,递归是一种强大的编程工具,能够简化复杂问题的解决过程。然而,递归同样需要谨慎使用,以免造成性能...

    基于C语言的递归算法研究.pdf

    尾递归是一种特殊的递归形式,如果递归调用是函数体中的最后一个动作,则编译器优化时可以将递归调用转变为跳转,避免新帧的创建,从而降低空间消耗。 文章作者刘卓亚是云南机电职业技术学院工业信息技术系的讲师,...

    C语言中递归的分析及应用.pdf

    3. 考虑尾递归优化,尾递归是指在函数返回的时候,调用自身本身,并且return语句不能包含表达式。这样的函数调用是最后一项操作,编译器或解释器可以进行优化,使递归本身无论调用多少次,都只占用一个栈帧,不会...

    C语言中递归算法的实现.pdf

    此时,可以考虑使用迭代的方法或者其他算法优化技术,如使用尾递归优化、记忆化递归等方法来减少递归调用的开销。在编写复杂的递归算法时,必须仔细测试和验证算法的正确性和效率,确保递归程序在各种情况下都能稳定...

    C语言函数递归调用PPT学习教案.pptx

    - 谨慎处理递归深度,避免栈溢出,可以考虑使用尾递归优化或迭代法代替。 - 在递归过程中,确保每次调用都是对问题规模的减小,否则可能无法收敛到正确答案。 总之,函数的递归调用是C语言中一个重要的编程技巧,...

    C.rar_instead5ss_尾递归_整数转为二进制数

    尾递归是一种优化的递归形式,它可以被编译器或解释器有效地处理,以减少内存栈的需求。在这个场景中,我们将深入探讨如何使用尾递归来实现整数到二进制的转换。 首先,让我们了解什么是尾递归。尾递归是指在一个...

    从尾到头打印链表(C语言实现)

    本话题主要关注如何使用C语言实现一个功能,即“从尾到头”打印链表,这个过程可能会涉及到递归和链表的反转操作。 首先,我们要理解链表的基本结构。一个链表节点通常包含两部分:数据域,存储元素值;指针域,...

    一些常见的递归示例: 计算阶乘 斐波那契数列 递归遍历树结构

    c语言 递归是编程中的一个强大概念,其中一个函数在其定义中调用自己。在 C 语言中,递归常用于解决那些可以被...尾递归优化:某些编译器能够优化尾递归,从而减少递归调用的栈空间,但这在 C 语言中并不总是得到支持。

    c语言课程笔记27.pdf

    6. **尾递归优化**:一些编译器(如GCC)支持尾递归优化,即在递归调用作为函数的最后一个操作时,可以重用当前栈帧,从而避免栈空间的线性增长。但这不是所有C语言实现的默认行为,因此编写递归函数时应考虑到这...

    递归与分治_有助理解递归

    在实现递归算法时,通常需要考虑优化,如使用迭代代替递归,或者使用尾递归等技术。 分治策略是递归的一种典型应用场景。它将大问题分解为若干个规模较小但结构相同的子问题,然后递归地解决这些子问题,最后将子...

    C语言递归算法程序.rar-综合文档

    此外,优化递归算法,如使用尾递归(tail recursion)或迭代(iteration)替代,可以提高程序性能。 综上所述,“C语言递归算法程序.rar”中的文档很可能涵盖了递归的基础概念、实例、优缺点、实现方法以及如何避免...

    C语言递归操作用法总结

    因此,在编写递归函数时,确保基础情况在最前面,并考虑是否可以通过尾递归或迭代等方式优化代码。 6. **其他示例**: 递归也可以用于其他领域,如字符串处理。判断一个字符串是否是回文(正读反读都一样的字符串)...

Global site tag (gtag.js) - Google Analytics