文档章节

C Primer Plus 第9章 函数 9.3 递归

idreamo
 idreamo
发布于 2016/07/12 06:27
字数 3661
阅读 80
收藏 0

9.3.1  递归的使用

为了具体说明,请看下面的例子。程序清单9.6中函数main()调用了函数up_and_down()。我们把这次调用称为“第一级递归”。然后up_and_down()调用其本身,这次调用叫做“第二级递归”。第2级递归调用第3级递归,依此类推。为了深入其中看看究竟发生了什么, 程序不仅显示出了变量n的值,还显出出了存储n的内存的地址&n(本章稍后部分将更全面的讨论&运算符。printf()函数使用%p说明符来指示地址)。

程序清单9.6  recur.c程序

/*recur.c --递归举例*/
#include <stdio.h>
void up_and_down(int);
int main(void)
{
    up_and_down(1);
    return 0;
}
void up_and_down (int n)
{
    printf("Level %d : n location %p\n",n,&n);  /*1*/
    if(n<4)
        up_and_down(n+1);
    printf("Level %d : n location %p\n",n,&n);  /*2*/
}

我们来分析程序中递归的具体工作过程。首先main()使用参数1调用了函数up_and_down()。于是up_and_down()中形式参量n的值为1,故打印语句#1输出了Leve 1。然后,由于n的数值小于4,所以up_and_down()(第1级)使用n+1即数值2调用了up_and_down()(第2级)。这使得n在第2级调用中被赋值2,打印语句#1输入的是Level 2。与之类似,下面的再次调用分别打印出Level 3和Level 4。

当开始执行第4级调用时,n的值是4,因此if语句的条件不满足。这时不再继续调用up_and_down()函数。第4级调用接着执行打印语句#2,即输出Level 4,因为n的值是4。现在函数需要执行return语句,此时第4级调用结束,把控制返回给该函数的调用函数,也就是第3级调用函数。第3级调用函数中前一个执行过后语句是在if语句中进行第4级调用。因此,它开始继续执行其后续的代码,即执行打印语句#2,这将会输出Level 3.当第3级调用结束后,第2级调用函数开始继续执行,即输出Level 2。依此类推。

注意,每一递归都使用它自己私有的变量n。你可以通过查看地上的值来得出这个结论(当然,不同的系统通常会以不同的格式显示不同的地址。关键点在于,调用时的Level 1地址和返回时的Level 1地址是相同的)。

如果您对此感到有些迷惑,可以假想进行了一系列的函数调用,即使用fun1()调用了fun2()、fun2()调用fun3(),fun3()调用fun4()。fun4()执行完后,fun3()会继续执行。而fun3()执行完后,开始执行fun2()。最后fun2()返回到fun1()中并执行后续的代码。递归过程也是如此,只不过fun1() fun2() fun3() fun4()都是相同的函数。

9.3.2  递归的基本原理

刚接触递归可能会感到迷惑,下面将讲述几个基本要点,以便于理解该过程:

第一,每一级的函数调用都有自己的变量。也就是说,第1级调用中的n不同于第2级调用中的n,因此程序创建了4个独立的变量,虽然每个变量的名字都是n,但是它们分别具有不同的值。当程序最终返回到对up_and_down()的第1级调用时,原来的n仍具有其初始值1.

第二,每一次函数调用都会有一次返回当程序流执行到某一级递归的结尾处时,它会转移到前1级递归继续执行。程序不能直接返回到main()中初始调用部分,而是通过递归的每一级逐步返回,即从up_and_down()的某一级递归返回到调用它的那一级。

第三,递归函数中,位于递归调用前的语句和各级被调函数具有相同的执行顺序。例如,在程序清单9.6中,打印语句#1位于递归调用语句之前。它按照递归调用的顺序被执行了4次,即依次为第1级、第2级、第3级、第4级。

第四,递归函数中,位于递归调用后的语句的执行顺序和各个被调用函数的顺序相反。例如,打印语句#2位于递归调用语句之后,其执行顺序是第4级、第3级、第2级、第1级。递归调用的这种特性在解决涉及反向顺序的编程问题时很有用。下文中将给出这样的一个例子。

第五,虽然每一级递归都有自己的变量,但是函数代码并不会得到复制。函数代码是一系列计算机指令,而函数调用就是从头执行这个指令集的下一条命令。一个递归调用会使程序从头执行相应函数的指令集。除了为每次调用创建变量,递归调用非常类似于一个循环语句。实际上,递归有时可被用来代替循环,反之亦然。

最后,递归函数中必须包含可以终止递归调用的语句。通常情况下,递归函数会使用一个if条件语句或其他类似的语句以便当函数参数达到某个特定值时结束递归调用。比如在上例中,up_and_down(n)调用 了up_and_down(n+1).最后,实际参数的值达到4时,条件语句if(n<4)得不到满足,从而结束递归。

9.3.4  尾递归

最简单的递归形式是把递归调用语句放在函数结尾即恰在return语句之前。这种形式被称作尾递归(tail recursion)或结尾递归(end recursion),因为递归出现在函数尾部。由于尾递归的作用相当于一条循环语句,所以它是最简单的递归形式。

下面我们讲述分别使用循环和尾递归完成阶乘计算的例子。一个整数的阶乘就是从1到该数的乘积。例如,3的阶乘(写作3!)是1X2X3。0的阶乘等于1,而且负数没有阶乘。程序清单9.7中,第一个函数使用for循环计算阶乘,而第二个函数用的是递归方法。

程序清单  9.7  factor.c程序

//factor.c  --使用循环和递归计算阶乘
#include <stdio.h>
long fact (int n);
long rfact (int n);
int main(void)
{
    int num;
    printf("This program calculates factorials.\n");
    printf("Enter a value in the range 0-12 (q to quit): \n");
    while (scanf("%d",&num)==1)
    {
        if(num<0)
            printf("No negative numbers,please.\n");
        else if (num>12)
            printf("Keep input under 13.\n");
        else
        {
            printf("loop: %d factorial = %ld\n",num,fact(num));
            printf("recursion: %d factorial = %ld\n",num,rfact(num));
        }
        printf("Enter a value in the range 0-12 (q to quit): \n");
    }
    printf("Bye.\n");
    return 0;
}

long fact(int n)    /*使用循环计算阶乘*/
{
    long ans;
    for(ans=1;n>1;n--)
        ans*=n;
    return ans;
}

long rfact(int n)    /*使用递归计算阶乘*/
{
    long ans;
    if(n>0)
        ans=n*rfact(n-1);
    else
        ans=1;

    return ans;
}

下面我们研究使用递归方法的函数。其中关键一点是n!=n x (n-1)!。因为(n-1)!是1到n-1的所有正数之积,所以该数乘以n就是n的阶乘。这也暗示了可以采用递归的方法。调用rfact()时,rfact(n)就等于n x rfact(n-1)。这样就可以通过rfact(n-1)来计算rfact(n),如程序清单9.7中所示。当然 ,递归必须在某个地方结束,可以在n为0时把返回值设为1,从而达到结束递归的目的。

在程序清单9.7中,两个函数的输出结果相同。虽然对rfact()的递归调用不是函数中的最后一行,但它是在n>0的情况下执行的最后一条语句,因此也属于尾递归。

既然循环和递归都可以用来实现函数,那么究竟选择哪一个呢?一般来讲,选择循环更好一些。首先,因为每次递归调用都拥有自己的变量集合,所以就需要占用较多的内存;每次递归调用需要把新的变量集合存储在堆栈中。其次,由于进行每次函数调用需要花费一定的时间,所以递归的执行速度较慢。既然如此,那么我们为什么还要讲述以上例子呢?因为尾递归是最简单的递归形式,比较容易理解;而且在某些时候,我们不能使用简单的循环语句代替递归,所以就有必要学习递归的方法。

9.3.4  递归和反向计算

下面我们来考虑一个使用递归处理反序的问题(在这类问题中使用递归比使用循环更简单)。

问题是这样的,编写一个函数将一个整数转换成二进制形式。二进制的意思是指数值以2为底数进行表示。

解决上述问题,需要使用一个算法(algorithm)。因为奇数的二进制形式的最后一位一定是1,而偶数的二进制数的最后一位是0,所以可以通过5%2得出5的进制形式中最后一位数字是1或者是0。一般来讲,对于数值n,其二进制数的最后一位是n%2因此计算出的第一个数字恰好是需要输出的最后一位。这就需要使用一个递归函数实现。在函数中,首先在递归调用之前计算n%2的数值然后在递归调用语句之后进行输出,这样计算出的第一个数值反而在最后一个输出。

为了得出下一个数字,需要把原数值除以2。这种计算就相当于在十进制下把小数点左移一位。如果此时得出的数值是偶数,则下一个二进制数是0;若得出的数值是奇数,则下一个二进制数是1.例如,5/2的数值是2(整数除法),所以下一位值是0。这时已经得到了数值01.重复以上计算,即使用2/2得出1,而1%2的数值是1,因此下一位数是1.这时得到的数值是101.那么何时停止这种计算呢?因为只要被2除的结果大于或等于2,那么就还需要一位二进制位进行表示,所以只有被2除的结果小于2时才停止计算。每次除以2就可以得出一位二进制位值,直到计算出最后一位为止。在程序清单9.8中实现以上算法:

程序清单9.8  binary.c程序

/*binary.c  --以二进制形式输出整数*/
#include <stdio.h>
void to_binary(unsigned long n);
int main(void)
{
    unsigned long number;
    printf("Enter an integer (q to quit): \n");
    while(scanf("%ul",&number)==1)
    {
        printf("Binary equivalent: ");
        to_binary(number);
        putchar('\n');
        printf("Enter an integer (q to quit): \n");
    }
    printf("Done.\n");
    return 0;
}
void to_binary(unsigned long n)/*递归函数*/
{
    int r ;
    r = n%2;
    if(n>=2)
        to_binary(n/2);
    putchar('0'+r);  /*以字符形式输出*/

    return 0;
}

以上程序中,如果r 是0,表达式‘0’+r就是字符‘0’;当r为1时,则该表达式的值为字符‘1’。得出这种结果的前提假设是字符‘1’的数值编码比字符‘0’的数值编码大1.ASCII和EBCDIC两种编码都满足上述条件。更一般的方式,你可以使用如下方法:

putchar(r ? '1' : '0' );

当然,不使用递归也能实现这个算法。但是由于本算法先计算出最后一位的数值,所以在显示结果之前必须对所有的数值进行存储。

9.3.5  递归的优缺点

优点是在于为某些编程问题提供了最简单的方法,而缺点是一些递归算法会很快耗尽内存。同时,使用递归的程序难于阅读和维护。从下面的例子,可以看出递归的优缺点。

斐波纳契数列定义如下:第一个和第二个数字都是1,而后续的每个数字是前两个数字之和。例如,数列中前几个数字是1,1,2,3,5,8,13.下面我们创建一个函数,它接受一个正整数n作为参数,返回相应的斐波纳契数值。

首先,关于递归深度,递归提供了一个简单的定义。如果调用函数Fionacci(),当n为1或2时Fabonacci(n)应返回1;对于其他数值应返回Fibonacci(n-1)+Fabonacci(n-2) :

long Fabonacci(int n)
{
    if(n>2)
        return Fibonacci(n-1)+Fibonacci(n-2);
    else 
        return 1;
}

这个C递归只是讲述了递归的数学定义。同时本函数使用了双重递归(double recursion);也就是说,函数对本身进行了两次调用。这就会导致一个弱点。

为了具体说明这个弱点,先假设调用函数Fibonacci(40)。第1级递归会创建变量n。接着它两次调用Fibonacci(),在第2级递归中又创建两个变量n。上述的两次调用中的每一次又进行了再次调用,因而在第3级调用中需要4个变量n,这时变量总数为7.因为每级调用需要的变量数是上级的两倍,所以变量的个数是以指数规律增长的!这种情况下,指数增长的变量数会占用大量内存,这就可能导致程序瘫痪。当然,以上是一个比较极端的例子,但它也表明了必须小心使用递归,尤其效率处于第一位时。

所有C函数地位同等(包括main()函数),每一个函数都可以调用其他任何函数或被其他任何函数调用。

© 著作权归作者所有

共有 人打赏支持
idreamo
粉丝 17
博文 139
码字总数 224743
作品 0
青岛
产品经理
私信 提问
初来乍到的新人而已

我是个新人,博客新人,也是编程的新人,我比较喜欢研究一些新奇的东西,数学的东西,我数学也确实不是很怎么样,但我觉得数学是个很神奇的东西,所以科学的基础,真正的计算机研究者必须懂数...

古藤昏鸦
2014/05/03
0
0
C Primer Plus 第9章 函数 9.11 编程练习答案

1、设计函数min(x,y)返回两个double数值中较小的数值,同时用一个驱动程序测试该函数。 2、 设计函数chline(ch,i,j),实现指定字符在i列到j列的输出,同时用一个驱动程序测试该函数。 3、编写...

idreamo
2016/07/19
232
0
C Primer Plus(5版)第8章编程题1_重定向实现

小伙伴们,对C语言编程有疑问的,可以加微信交流:poo_poo或者扫描我的头像,验证时请注明是“知友” 一、题目描述 本题是第8章编程题的第一道题,题目如下: 二、题目及思路分析 从题述来看...

石家的鱼
2017/07/11
0
0
clisp运行错误--ANSI Common Lisp 课后习题5.5

这是ANSI Common Lisp第5章第5个练习题。题目原文为: Define iterative and recursive versions of a function that takes an object x and vector v, and returns a list of all the objec......

zcj
2012/05/27
967
2
【书评:Oracle查询优化改写】第五至十三章

【书评:Oracle查询优化改写】第五至十三章 一.1 BLOG文档结构图 一.2 前言部分 一.2.1 导读 各位技术爱好者,看完本文后,你可以掌握如下的技能,也可以学到一些其它你所不知道的知识,~O(∩...

技术小胖子
2017/11/15
0
0

没有更多内容

加载失败,请刷新页面

加载更多

一致性hash和虚拟节点

consistent hashing 算法的原理 consistent hashing 是一种 hash 算法,简单的说,在移除 / 添加一个 cache 时,它能够尽可能小的改变已存在key 映射关系,尽可能的满足单调性的要求。 下面就...

群星纪元
32分钟前
2
0
说一下Dubbo 的工作原理?注册中心挂了可以继续通信吗?

面试题 说一下的 dubbo 的工作原理?注册中心挂了可以继续通信吗?说说一次 rpc 请求的流程? 面试官心理分析 MQ、ES、Redis、Dubbo,上来先问你一些思考性的问题、原理,比如 kafka 高可用架...

李红欧巴
42分钟前
19
0
腾讯面试:一条SQL语句执行得很慢的原因有哪些?

说实话,这个问题可以涉及到 MySQL 的很多核心知识,可以扯出一大堆,就像要考你计算机网络的知识时,问你“输入URL回车之后,究竟发生了什么”一样,看看你能说出多少了。 之前腾讯面试的实...

java菜分享
今天
11
0
Java 基本功 之 CAS

本文首发于个人公众号《andyqian》, 期待你的关注! 前言 在Java并发编程中,我们经常使用锁对竞争资源予以并发控制,以解决资源竞争的问题。但无论是使用 Lock 还是 Synchronized,随着锁机...

andyqian
今天
5
0
信号量与条件变量的区别

注意信号量与条件变量的区别 信号量内容可见:http://www.cnblogs.com/charlesblc/p/6142868.html 信号量、共享内存,以及消息队列等System V IPC三剑客主要关注进程间通信; 而条件变量、互...

shzwork
今天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部