1、递归是什么?
递归是学习C语言函数绕不开的一个话题,那什么是递归呢?
递归其实是一种解决问题的方法,在C语言中,递归就是函数自己调用自己。
写一个史上最简单的C语言递归代码:
#include <stdio.h>
int main()
{
printf("hehe\n");
main();//main函数中又调用了main函数
return 0;
}
上述就是一个简单的递归程序,只不过上面的递归只是为了演示递归的基本形式,不是为了解决问题,代码最终也会陷入死循环,导致栈溢出。
递归的思想:
把一个大型复杂问题层层转化为一个与原问题相似,但规模较小的子问题来求解;直到子问题不能再被拆解,递归就结束了。所以递归的思考方式就是把大事化小的过程。
递归中的递是递推的意思,归就是回归的意思。
注:所以使用递归需要限制条件,达到递归限制条件,就返回,避免死递归,导致栈溢出。
2、递归的限制条件
递归在书写的时候,有2个必要条件:
- 递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
在下面的例子中,我们逐步体会这2个限制条件。
3、递归举例
3.1 举例1:求n的阶乘
计算n的阶乘(不考虑移除),n的阶乘就是1-n的数字累积相乘.
3.1.1 分析和代码实现
我们知道n的阶乘的公式:n! = n*(n - 1)!
举例:
5! = 5 * 4!(4*3*2*1)
4! = 4 * 3!(3*2*1)
3! = 3 * 2!(2*1)
2! = 2 * 1!(1)
1! = 1
所以:5! = 5 * 4!
这样的思路就是把一个较大的问题,转换为一个与原问题相似,但规模较小的问题来求解
n!---->n * (n-1)!
直到n是1或者0时,不再拆解。
所以可以使用递归的方式求n的阶乘:
#include <stdio.h>
int Fact(int n)
{
if (n <= 1)
return 1;
else
return n*Fat(n - 1);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fact(n);
printf("%d\n", ret);
return 0;
}
运行结果:
这个阶乘就是递归实现,每次进入函数先判断,如果n大于1就递归,每次递推就需要这个变量n不停的接近这个判断条件。如果等于或小于则作为结束递推的条件结束递推,接下来就是不停的回归。
3.1.2 画图推演
3.2举例2:顺序打印一个整数的每一位
题目:输入一个整数,打印它的每一位。
例如:输入1234 输出1 2 3 4
输入520 输出5 2 0
3.2.1 分析和代码实现
思路是每次%它的个位数,再让这个数/10,除去原来个位的位数。但是输入1234打印的却是4 3 2 1,因为这个是先%最后一位,在找前面几位,打印结果和我们想要输出的结果相反,怎么办?那如果我们将4 3 2 1 整合成一个值再取位数拿到1 2 3 4不就行了吗?虽然可行但是效率确极低。这时候就可以使用到递归。
递归思路:先不停递推找到第一位数,取模后在不停回归取模当前个位数然后就可以输出正确顺序的每一位了。
print(1234)
==>print(123) +printf(4)
==>print(12) +printf(3)
==>print(1) +printf(2)
==>printf(1)
函数递归实现:
#include <stdio.h>
void print(int n)
{
if (n > 9)
print(n / 10);
printf("%d ", n % 10);
}
int main()
{
int n = 0;
scanf("%d", &n);
print(n);
return 0;
}
运行结果:
3.2.2 画图推演
4、递归与迭代
递归是一种很好的编程技巧,但是很多技巧一样,也可能是被误用的,就像举例1一样,看到推导公式,很容易就被写成递归的形式
int Fact(int n)
{
if(n <= 0)
return 1;
else
return n*Fact(n-1);
}
Fact函数是可以产生正确的结果,但是在递归函数调用的过程中涉及一些运行时开销。
在C语言中每一次函数调用,都需要为本次函数调用在栈区上申请一块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧。
函数不返回,函数对应的栈帧空间就一直占用,所以如果函数调用中存在递归调用的话,每一次递归函数调用都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。
所以如果采用函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间, 也可能引起栈溢出 (stack over flow) 的问题。
如果不想使用递归就得想其他的办法,通常就是迭代的方式(通常就是循环的方式)。
比如:计算n的阶乘,也是可以产生1-n的数字累积乘在一起的:
int Fart(int n)
{
int i = 0;
int ret = 1;
for(i = 1;i <= n;i++)
{
ret *= i;
}
return ret;
}
上述代码是能够完成任务,并且效率是比递归的方式更好的。
事实上,我们看到的许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更加清晰,但是这些问题的迭代实现往往比递归实现效率更高。
当一个问题非常复杂,难以使用迭代的方式实现时,此时递归的简洁性便可以补偿它所带来的运行时开销。
举例3:求第n个斐波那契数
我们也能举出更加极端的例子,就像计算第n个斐波那契数,是不适合使用递归求解的,但是斐波那契数的问题通过是使用递归的形式描述的,如下:
看到这公式,很容易诱导我们将代码写成递归的形式,如下所示:
#include <stdio.h>
int Fib(int n)
{
if (n <= 2)
return 1;
else
return Fib(n - 1) + Fib(n - 2);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
运行结果:
结果看来虽然是对的,如果将数值输入50,那就不能输出正确的斐波那契数值了,这个数庞大到超过千亿甚至更多,这么庞大的数字在C语言中没有任何一个类型变量可以接收这么大的值,这么庞大的数顶多只能用字符来表示,但是这不是最主要的,因为需要不停的递归运算,需要递归不知多少亿次,计算机、CPU再牛也不可能一下就运算出来,又是运算又是开辟空间需要一段时间,不仅效率低下给出的值显示出来的也不正确。如果是迭代,就算输入100也能在1秒内给你输入出一个值,虽然也不对,但是可以得知迭代运行效率确实比递归高,所以想让程序运行效率高时用迭代,遇到迭代难以实现的复杂代码时,递归的简洁性就弥补了运行时开销带来的效率低下问题。
迭代实现斐波那契数:
#include <stdio.h>
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 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\n", ret);
return 0;
}
迭代实现第n个斐波那契数的运行效率效率就要高出很多。
那什么时候使用递归法什么时候使用迭代法呢?
1、如果一个问题使用递归方式去写代码,是非常方便的,简单的写出的代码是没有明显缺陷的,这个时候使用递归就可以
2、如果使用递归写的代码是存在明显缺陷的
比如:栈溢出、效率低下等
这时候考虑其他方式,比如:迭代
有时候,递归虽好,但是也是会引入一些问题,所以我们一定不要迷恋递归,适可而止就好。
C语言笔记第6篇结束,再见