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。oop
当开始执行第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地址是相同的)。ui
若是您对此感到有些迷惑,能够假想进行了一系列的函数调用,即便用fun1()调用了fun2()、fun2()调用fun3(),fun3()调用fun4()。fun4()执行完后,fun3()会继续执行。而fun3()执行完后,开始执行fun2()。最后fun2()返回到fun1()中并执行后续的代码。递归过程也是如此,只不过fun1() fun2() fun3() fun4()都是相同的函数。编码
9.3.2 递归的基本原理code
刚接触递归可能会感到迷惑,下面将讲述几个基本要点,以便于理解该过程:递归
第一,每一级的函数调用都有本身的变量。也就是说,第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()函数),每个函数均可以调用其余任何函数或被其余任何函数调用。