递归

C允许函数调用它自己,这种调用过程称为递归(recursion)。 递归有时难以捉摸,有时却很方便实用。结束递归是使用递归的难点,因为如果递归代码中没有终止递归的条件测试部分,一个调用自己的函数会。无限递归。
可以使用循环的地方通常都可以使用递归。有时用循环解决问题比较好,但有时用递归更好。递归方案更简洁,但效率却没有循环高。

递归演示

void Recursion(int n)
{
    printf_s("Level  %d : n location %p\n", n, &n);//#1
    if (n < 4)
    {
        Recursion(n + 1);
    }
    printf_s("Level  %d : n location %p\n", n, &n);//#2
}
int main()
{
	Recursion(1);
	return 0;
}

在这里插入图片描述
我们来仔细分析程序中的递归是如何工作的。首main()调用了带参数1的Recursion(),执行结果是Recursion() 中的形式参数n的值是1,
所以打印语句#1打印Level 1。然后,由于n小于4,Recursion()(下一级)调用实际参数为n十1(2)的,于是第2级调用中的n的值是2,打印语句#1打印Level 2。此类似,下面两次调用打印的分别是Level 3和Level 4。

当执行到第4级时,n的值是4,所以if测试条件为假。Recursion()函数不再调用自己。第4 级调用接着执行打印语句#2,即打印LEVEL 4,因为n的值是4。此时,第4级调用结束,控制被传回它的主调函数(即第3级调用)。在第3级调用中,执行的最后一条语句是调用 if语句中的第4级调用。被调函数(第4级调用)把控制返回在这个位置,因此,第3级调用继续执行后面的代码,打印语句#2打印LEVEL 3。然后第3级调用结束,控制被传回第2级调用,接着打印LEVEL 2,以此类推。

注意,每级递归的变量n都属于本级递归私有。这从程序输出的地址值可以看出(当然,不同的系统表示的地址格式不同,这里关键要注意,Level 1和Level 1的地址相同,Level 2和Level 2的地址相同,等等)。

递归的基本原理

第1,每级函数调用都有自己的变量。也就是说,第1级的n和第2级的n不同,所以程序创建了4个单独的变量,每个变量名都是n,但是它们的值各不相同。当程序最终返回Recursion()的第1级调用时,最初的n仍然是它的初值1。

变量nnnn
第一级调用后1
第二级调用后12
第三级调用后123
第四级调用后1234
从第四级返回后123
从第三级返回后12
从第二级返回后1
从第一级返回后

第2,每次函数调用都会返回一次。当函数执行完毕后,控制权将被传回上一级递归。程序必须按顺序逐级返回递归,从某级Recursion()返回上一 级的Recursion(),不能跳级回到main()中的第1级调用。

第3,递归函数中位于递归调用之前的语句,均按被调函数的顺序执行。例如,程序中的打印语句#1位于递归调用之前,它按照递归的顺序:第1级、第2级、第3级和第4级,被执行了4次。

第4,递归函数中位于递归调用之后的语句,均按被调函数相反的顺序执行。例如,打印语句#2位于递归调用之后,其执行的顺序是第4级,第3级、第2级、第1级。递归调用的这种特性在解决涉及相反顺序的编程问题时很有用。

第5,虽然每级递归都有自己的变量,但是并没有拷贝函数的代码。程序按顺序执行函数中的代码,而递归调用就相当于又从头开始执行函数的代码。除了为每次递归调用创建变量外,递归调用非常类似于一个循环语句。实际上,递归有时可用循环来代替,循环有时也能用递归来代替。

最后,递归函数必须包含能让递归调用停止的语句。通常,递归函数都使用if或其他等价的测试条件,在函数形参等于某特定值时终止递归。为此,每次递归调用的形参都要使用不同的值。例如,程序中的Recursion(n)调用Recursion(n+1)。 最终,实际参数等于4时,if 的测试条件(n < 4)为假。

尾递归

最简单的递归形式是把递归调用置于函数的末尾,即正好在return语句之前。这种形式的递归被称为尾递归(tail recursion),因为递归调用在函数的末尾。尾递归是最简单的递归形式,因为它相当于循环。
下面要介绍的程序示例中,分别用循环和尾递归计算阶乘。一个正整数的阶乘 (factorial) 是从1到该整数的所有整数的乘积。例如,3的阶乘(写作3!)是1x2x3。另外,0!等于1,负数没有阶乘。程序中,第1个函数使用for循环计算阶乘,第2个函数使用递归计算阶乘。

int fact(int n)//使用循环
{
    int ans = 1;
    for (ans = 1; n > 1; n--)
    {
        ans = ans * n;
    }
    return ans;
}
int rfact(int n)//使用递归
{
    int ans = 1;
    if (n > 0)
    {
        ans = n * rfact(n - 1);
    }
    else
    {
        ans = 1;
    }
    return ans;
}

使用循环的函数把ans初始化为1.然后把ans与从n~2的所有递减整数相乘。根据阶乘的公式。还应该乘以1,但是这并不会改变结果。

现在考虑使用递归的函数。该函数的关键是n! = n* (n-1)!。可以这样做是因为(n-1) !是n-1~1的所有正整数的乘积。阶乘的这一特性很适合使用递归。 如果调用函数rfact(), rfact(n)是n*fact(n-1)。因此,通过调用rfact(n-1) 来计算rfact(n), 如程序中所示。当然,必须要在满足某条件时结束递归, 可以在n等于0时把返回值设为1。

程序中使用递归的输出和使用循环的输出相同。注意,虽然rfact ()的递归调用不是函数的最后行,但是当n>0时,它是该函数执行的最后一 条语句, 因此它也是尾递归。

既然用递归和循环来计算都没问题,那么到底应该使用哪一个? 一般而言, 选择循环比较好。首先,每次递归都会创建一组变量, 所以递归使用的内存更多,而且每次递归调用都会把创建的一组新变量放在栈中。递归调用的数量受限于内存空间。其次,由于每次函数调用要花费一定的时间, 所以递归的执行速度较慢。那么,演示这个程序示例的目的是什么?因为尾通归是递归中最简单的形式,比较容易理解,在某些情况下,不能用简单的循环代替递归。

递归和倒序计算

递归在处理倒序时非常方便(在解决这类问题中,递归比循环简单)。我们要解决的问题是:编写个函数,打印一个整数的二进制数。 一进制表示法根据 2的幂来表示数字。

我们要设计一个以二进制形式表示整数的方法或算法 (algorithm)。例如,以十进制数5来说。在二进制中,奇数的末尾定是1, 偶数的末尾一定是0, 所以通过5%2即可确定5的二进制数的最后一后位是1还是0。一般而言,对于数字n,其二进制的最后一 位是n%2。因此,计算的第一位数字实际上是待输出二进制数的最后一位。 这规律提示我们, 在递归函数的递归调用之前计算n%2,递归调用之后打印计算结果。这样,计算的第1个值正好是最后一个打印的值。

要获得下一位数字,必须把原数除以2。 这种计算方法相当于在十进制下把小数点左移一位,如果计算结果是偶数,那么二进制的下一位数就是0,如果是奇数,就是1。例如,5/2得2 (整数除法),2是偶数(2%2得0),所以下一位二进制数是0。到目前为止,我们已经获得01。继续重复这个过程。2/2得1,1%2得1,所以下一位二进制数是1。因此,我们得到5的等价二进制数是101. 那么,程序应该何时停止计算?当与2相除的结果小于2时停止计算,因为只要结果大于或等于2,就说明还有二进制位。每次除以2就相当于去掉一位二进制,直到计算出最后一位为止。

void to_bin(int n)
{
	int cur=n % 2;
	if(n >= 2) to_bin(n/2);
	printf("%d",cur);
}

不用递归,是否能实现这种用二进制形式表示整数的算法?当然可以。但是由于这种算法要首先计算最后一位二进制数,所以在显示结果之前必须把所有的位数都储存在别处(例如,数组)。

递归的优缺点

递归既有优点也有缺点。优点是递归为某些编程问题提供了最简单的解决方案。缺点是一些递归算法会快速消耗计算机的内存资源。另外,递归不方便阅读和维护。我们用一个例子 来说明递归的优缺点。

斐波那契数列的定义如下:第1个和第2个数字都是1,而后续的每个数字都是其前两个数字之和。
例如,该数列的前几个数是: 1、1、2、3、5、8、13。下面, 我们要创建一个函数, 接受正整数n,返回相应的斐波那契那契数值。
首先,来看递归。递归提供个一简单的定义。如果把函数命名为Fibonacci(),那么如果n只是1或2。Fibonacci (n)应返回1,对于其他数值,则应返回Fibonacci(n-1)+Fibonacci (n-2).

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

这个递归函数只是重述了数学定义的递归。该函数使用了双递归(double recursion)即的数每一级道归都要调用本身两次。这暴露了一个问题。为了说明这个问题,假设调用Fibonacci(40)。这是第1级递归调用,将创建一个变量 n,然后在该函数中要调用Fibonacci ()两次,在第2级递归中要分别创建两个变量n。这两次调用中的每次调用又会进行两次调用,因而在第3级递归中要创建4个名为n的变量。此时总共创建了7个变量。由于每级递归创建的变量都是上一级递归的两倍, 所以变量的数量呈指数增长!按指数增长很快就会产生非常大的值。在本例中,指数增长的变量数量很快就消耗掉计算机的大量内存,很可能导致程序崩溃。
虽然这是个极端的例子,但是该例说明:在程序中使用递归要特别注意,尤其是效率优先的程序。

所有的C函数皆平等

程序中的每个C函数与其他函数都是平等的。每个函数都可以调用其他函数,或被其他函数调用,这点与Pascal和Modula-2中的过程不同,虽然过程可以嵌套在另一个过程中,但是嵌套在不同过程中的过程之间不能相互调用。

main()函数是否与其他函数不同?是的,main()的确有点特殊。当main()与程序中的其他函数放在一起时,最开始执行的是main()函数中的第1条语句,但是这也是局限之处。main()也可以被自己或其他函数递归调用——尽管很少这样做。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值