递归与尾递归(C语言)

原文: 递归与尾递归(C语言)【转】

 做者:archimedeshtml

本文版权归做者和博客园共有,欢迎转载,但未经做者赞成必须保留此段声明,且在文章页面明显位置给出原文链接,不然保留追究法律责任的权利.

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

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

通常来讲,递归须要有:边界条件、递归前进段和递归返回段。并发

当边界条件不知足时,递归前进;当边界条件知足时,递归返回。编程语言

注意:函数

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

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

本文地址:http://www.cnblogs.com/archimedes/p/rescuvie-tailrescuvie.html,转载请注明源地址。优化

c程序在虚拟内存中的地址从低地址到高地址的顺序依次是.text段(代码区)、.rodata段(常量区)、.data段(已初始化的全局变量区)、.bss段(未初始化的全局变量区)、堆、动态库映射区、栈、内核区(用户态代码不可访问)url

基本递归

问题:计算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中(亦是栈语言),若是想要保持语言的高并发特性,就必须用尾递归来替代传统的递归。

 

做者: archimedes
本文版权归做者和博客园共有,欢迎转载,但未经做者赞成必须保留此段声明,且在文章页面明显位置给出原文链接,不然保留追究法律责任的权利.
相关文章
相关标签/搜索