了解函数递归

有人可能会问:什么叫递归?大家不用觉得很难,其实递归就是一种解决问题的方法,在C语言中,递归就是函数自己调用自己。

递归的好处:

1.递归往往只用少量的代码,就完成了大量的计算。

2.有些场景下,递归代码写起来非常方便


首先,来看一个比较简单的递归代码:

#include <stdio.h>
int main()
{
	printf("hehe\n");
	main();

	return 0;
}

上述就是⼀个简单的递归程序,只不过上⾯的递归只是为了演⽰递归的基本形式,不是为了解决问 题,代码最终也会陷⼊死递归,导致栈溢出(Stackoverflow)。

这里可能会有小伙伴问:什么叫栈溢出?别着急,后面会娓娓道来。


 递归的思想和限制条件

递归的思想:

把⼀个⼤型复杂问题层层转化为⼀个与原问题相似,但规模较⼩的⼦问题来求解;直到⼦问题不能再 被拆分,递归就结束了。所以递归的思考⽅式就是把⼤事化⼩的过程。

递归中的递就是递推的意思,归就是回归的意思,接下来慢慢来体会。 


递归的限制条件:

递归在书写的时候,有2个必要条件:

递归存在限制条件,当满⾜这个限制条件的时候,递归便不再继续

每次递归调⽤之后越来越接近这个限制条件

在下⾯的例⼦中,我们逐步体会这2个限制条件。 


 递归的举例

举例1:求n的阶乘

题⽬:计算n的阶乘(不考虑溢出),n的阶乘就是1~n的数字累积相乘。

 首先,先分析问题,阶乘的公式为n != n∗(n−1)!,我们可以把他分成(n - 1)! = (n - 1)*(n - 2)!, (n - 2)! = (n - 2)*(n - 3)!......这样把问题递推下去,直到n是1或者0时,不在拆解,用规模较小的问题解决。

再稍微分析一下,当 n == 0 的时候,n的阶乘是1,其余n的阶乘都是可以通过公式计算。

#include <stdio.h>
int Fact(int n)
{
	if (0 == n)
	{
		return 1;
	}
	else
		return n * Fact(n - 1); //函数自己调用自己
}

int main()
{
	int i = 0;
	scanf("%d", &i);
	int r = Fact(i);
	printf("%d ", r);

	return 0;
}

 运⾏结果(这⾥不考虑n太⼤的情况,n太⼤存在溢出):


问题的解释和分析

问题一:上面我多次提到栈溢出,那到底什么是栈溢出?

答:在C语言中,每一次的函数调用,都会在栈区申请一块内存空间,申请的空间叫:运行时堆栈挥着函数栈帧空间,用来为这次函数调用存放信息。等把栈区的空间占满,这就是栈溢出。


问题2:这个程序是怎么运行的? 

 答:当n==5时,不满足if条件,就一直走else,直到n == 0返回了1,就回归,倒着走。


 解决方法

像上面这种,如果算很大的阶乘,递归的层次太深,就会浪费太多的栈帧空间,就会引起栈溢出的问题。这时候,我们就得使用迭代的方式(循环)。

⽐如:计算n的阶乘,也是可以产⽣1~n的数字累计乘在⼀起的。

#include <stdio.h>
int main()
{
	int i = 0;
	int ret = 1;
	int n = 0;
	scanf("%d", &n);
	for (i = 1; i <= n; i++)
	{
		ret *= i;
	}
	printf("%d ", ret);
	return 0;
}

上述代码是能够完成任务,并且效率是⽐递归的⽅式更好的。 

 事实上,我们看到的许多问题是以递归的形式进⾏解释的,这只是因为它⽐⾮递归的形式更加清晰, 但是这些问题的迭代实现往往⽐递归实现效率更⾼。

当⼀个问题⾮常复杂,难以使⽤迭代的⽅式实现时,此时递归实现的简洁性便可以补偿它所带来的运 ⾏时开销


举例2:顺序打印⼀个整数的每⼀位

输⼊⼀个整数m,按照顺序打印整数的每⼀位。

⽐如:

输⼊:1234    输出:1  2 3 4

输⼊:520    输出:5 2 0 


问题与解答

问题1:当我们输入一个数的时候,怎么得到这个数的每一位呢?

答:1234%10就能得到4,然后1234/10得到123,这就相当于去掉了4。 然后继续对123%10,就得到了3,再除10去掉3,以此类推 不断的 %10 和 /10 操作,直到1234的每⼀位都得到;


 问题2: 顺序怎么打印呢?

答:我们发现其实⼀个数字的最低位是最容易得到的,通过%10就能得到 那我们假设想写⼀个函数Print来打印n的每⼀位。


 代码实现

#include <stdio.h>
void Print(int n)
{
	if (n > 9) //当只有一位数时,直接打印
		Print(n / 10);
	printf("%d ", n % 10);
}
int main()
{
	int i = 0;
	printf("输入一个整数:>");
	scanf("%d", &i);
	Print(i);

	return 0;
}

首先,print函数要走俩行代码,第一是if ,第二是printf,当输入1234,调用了Print(123)....直到Print(1)不符合了if,走printf打印了1,然后回归,执行了12%10 ,打印了2。依次打印了3 ,4.

大家可以参看一下图,在深刻理解一下。 


 举例3:求n个斐波那契数列

我们也能举出更加极端的例⼦,就像计算第n个斐波那契数,是不适合使⽤递归求解的,但是斐波那契 数的问题通过是使⽤递归的形式描述的,如下:

看到这公式,很容易诱导我们将代码写成递归的形式,如下所⽰: 

#include <stdio.h>
int count = 0;
int Fib(int n)
{
	if (n == 3)
		count++;
	if (n <= 2)
		return 1;
	else
		return Fib(n - 1) + Fib(n - 2);
}
int main()
{
	int i = 0;
	scanf("%d", &i);
	int c = Fib(i);
	printf("%d\n", c);
	printf("count = %d\n", count);
	return 0;
}

这⾥我们看到了,在计算第40个斐波那契数的时候,使⽤递归⽅式,第3个斐波那契数就被重复计算了 39088169次,这些计算是⾮常冗余的。所以斐波那契数的计算,使⽤递归是⾮常不明智的,我们就得 想迭代的⽅式解决 


我们知道斐波那契数的前2个数都1,然后前2个数相加就是第3个数,那么我们从前往后,从⼩到⼤计 算就⾏了。 

#include <stdio.h>
int Fib(int n)
{
	int a = 1;
	int b = 1;
	int c = a + b;
	if (n > 0 && n <= 2)
		return 1;
	while (n > 2)
	{
		c = a + b;
		a = b;
		b = c;
		
		n--;
	}
	return c;
}
int main()
{
	int n = 0;
	scanf("%d", &n);
	int ret = Fib(n);
	printf("%d ", ret);
}

 迭代的⽅式去实现这个代码,效率就要⾼出很多了。 有时候,递归虽好,但是也会引⼊⼀些问题,所以我们⼀定不要迷恋递归,适可⽽⽌就好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值