目录
前言
学习完c语言的循环和调试技巧后,发现如果程序过长的化调试起来会十分繁琐,并且程序过长也略微不咋好看,那么有没有什么方法只用一段简短的程序就能实现特定的功能呢?本篇文章将给你介绍名为递归的方法,让你感受它的魅力。
提示:以下是本篇文章正文内容,下面案例可供参考
一、递归是什么?
递归是学习C语言函数绕不开的⼀个方话题,那什么是递归呢?
递归其实是⼀种解决问题的方法,在C语言中,递归就是函数函数自己调用自己。对它析词解字的话就是,递归中的递就是传递的意思,归就是回归的意思。下面用一串代码将递归呈现出来:
#include <stdio.h>
int main()
{
printf("hehe\n");
main();//main函数中⼜调⽤了main函数
return 0;
}
上述就是⼀个简单的递归程序,演示了递归的基本形式,不是为了解决问题,代码最终也会陷⼊死递归,不断地打印hehe,最终导致栈溢出(Stack overflow)。
1.1递归思想:
把⼀个大型复杂问题层层转化为⼀个与原问题相似,但规模较小的子问题来求解;直到⼦问题不能再被拆分,递归就结束了。所以递归的思考方式就是把大事化小的过程;可以用剥洋葱来想象这一过程。
下面用一个类比来帮助你形象地理解递归的过程,现在想象有一排小矮人,而你有一个篮子(假设无限大),想知道这一排小矮人一共采了多少苹果;那么你需要将篮子从排头的小矮人一直传到最后一个,然后由最后一个小矮人把苹果装入篮子,再把篮子给他前一个小矮人并告诉他苹果数目;这个小矮人会将自己采的苹果数目与这个数相加,再告诉前一个小矮人,直到排头的小矮人把苹果装入篮子交给你并告诉你苹果总数目,那么此时你将获得小矮人们采的所有苹果数(问题转化——将数苹果总数问题转化为局部数目相加的小问题)。
1.2递归的限制条件
为了避免程序进入死循环,需要给递归加入一定的限制,这样才能够“回归”。
递归在书写的时候,有2个必要条件:
•递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。
•每次递归调用之后越来越接近这个限制条件。
在下面的两个例子中,让我们逐步体会这2个限制条件。
1.3.举例一:求n的阶乘
计算n的阶乘(不考虑溢出),n的阶乘就是1~n的数字累积相乘。
1.3.1分析和代码实现
我们知道n的阶乘的公式: n! = n ∗ (n − 1)!
举例:
5! = 5*4*3*2*1
4! = 4*3*2*1 所以:5! = 5*4!
3!= 3*2*1
2!= 2*1
1!= 1
以此发现:n!=(n-1)!,可以将问题逐渐转化
这样的思路就是把⼀个较大的问题,转换为⼀个与原问题相似,但规模较小的问题来求解的。
n!---> n*(n-1)!
(n-1)! ---> (n-1)*(n-2)!
......
直到n是1或者0时,不再拆解
再稍微分析⼀下,当 n<=1 的时候,n的阶乘是1,其余n的阶乘都是可以通过上述公式计算
n的阶乘的递归公式如下:
那么可以由递归公式写出下列代码:
int Fact(int n) {
int ret = 0;
if (n > 1) {
return n * Fact(n - 1);
}
else {
return 1;
}
}
为了验证程序的准确性,采取个别例子来检验,其中不考虑n太大的问题,n太大会造成栈溢出。代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int Fact(int n) {
int ret = 0;
if (n > 1) {
return n * Fact(n - 1);
}
else {
return 1;
}
}
int main() {
int jie = 0;
int out = 0;
printf("请输入所求的阶乘数:");
scanf("%d", &jie);
out = Fact(jie);
printf("\n所求的阶乘数为%d",out);
return 0;
}
运行结果如下:
1.3.2.递归过程图示
流程图演示:
代码块演示:
1.4.举例二:顺序打印⼀个整数的每⼀位
输入⼀个整数m,打印这个按照顺序打印整数的每⼀位。
比如:
输入:1234 输出:1 2 3 4
输入:520 输出:5 2 0
1.4.1问题分析和代码实现
用例子:123来说,
当看到这一题目时,第一想法是将123/100=1,123%10=23,23/10=2,3,这样就能获得该数的每一位,但并非每一个都是三位,它的位数是不确定的,而可以确定的是对数处理的操作是类似的,除10,对10取余数。那么带着这种思想,就找到了这题的切入点。
bit_num(123)
==>bit_num(12) + bit_num(3)
==>bit_num(1) + bit_num(2)
==>bit_num(1)
既然在不确定位数的情况下不能准确的找到高位,那么去最低位如何呢?利用除法和取余操作,由取余可以获取低位,除10可以排除低位的,可以得到下列代码:
void bit_num(int a) {
if (a / 10 == 0) {
printf("%d ", a);
}
else {
bit_num(a / 10);
printf("%d ", a % 10);
}
}
当生最高位只有一个数时(即n<9),n=n%10,那么可以合并printf项,简化后:
void bit_num(int n) {
if (n >9) {
bit_num(n / 10);
}
printf("%d ", n % 10);
}
在这个解题的过程中,我们就是使用了大事化小的思路
把bit_num(123) 打印123每⼀位,拆解为首先bit_num(12)打印12的每⼀位,再打印得到的3,
把bit_num(12) 打印12每⼀位,拆解为首先Print(1)打印1的每⼀位,再打印得到的2
直到bit_num打印的是⼀位数,直接打印就行。
1.4.2 画图推演
以123每⼀位的打印来推演⼀下:
二、递归与迭代
递归是⼀种很好的编程技巧,但是很多技巧⼀样,也是可能被误用的,就像举例1⼀样,看到推导的公式,很容易就被写成递归的形式:
int Fact(int n) {
int ret = 0;
if (n > 1) {
return n * Fact(n - 1);
}
else {
return 1;
}
}
Fact函数是可以产生正确的结果,但是在递归函数调用的过程中涉及⼀些运行时的开销。
在C语言中每⼀次函数调用,都要需要为本次函数调用在栈区申请⼀块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧。
函数不返回,函数对应的栈帧空间就⼀直占用,所以如果函数调⽤中存在递归调⽤的话,每⼀次递归
函数调用都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。所以如果采⽤函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stack overflow)的问题。
注:
所以如果不想使用递归就得想其他的办法,通常就是迭代的方式(通常就是循环的方式,换句话来说就是不断地重复做一件事)。
比如:计算n的阶乘,也是可以产生1~n的数字累计乘在⼀起的。
int Fact(int n) {
int ret = 0;
if (n > 1) {
return n * Fact(n - 1);
}
else {
return 1;
}
}
事实上,我们看到的许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更加清晰,但是这些问题的迭代实现往往比递归实现效率更高。
当⼀个问题非常复杂,难以使用迭代的方式实现时,此时递归实现的简洁性便可以补偿它所带来的运行时开销
2.1.举例三:求第n个斐波那契数列
通过上面两个例子,你会发现递归程序是不是是否简洁,那么是不是无论遇到什么问题都可以用递归解决呢?下面计算第n个斐波那契数,是不适合使用递归求解的,但是斐波那契数的问题通过是使用递归的形式描述的,如下:
看到这公式,很容易诱导我们将代码写成递归的形式,如下所示:
int find(int n) {//递归
int temp = 0;
if (n > 2) {
temp = find(n - 1) + find(n - 2);
}
else {
temp = 1;
}
return temp;
}
全代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int find(int n) {//递归
int temp = 0;
if (n > 2) {
temp = find(n - 1) + find(n - 2);
}
else {
temp = 1;
}
return temp;
}
int main() {
int input = 0;
int number = 0;
printf("请输入想知道的第n个斐波那契数列: \n");
scanf("%d", &input);
number = find(input);
printf("%d\n", number);
return 0;
}
我们n输入为50的时候,需要很长时间才能算出结果,这个计算所花费的时间,是我们很难接受的,这也说明递归的写法是非常低效的,那是为什么呢?
这里我们看到了,在计算第40个斐波那契数的时候,使用递归方式,第3个斐波那契数就被重复计算了39088169次,这些计算是⾮常冗余的。
所以斐波那契数的计算,使用递归是非常不明智的,我们就得想迭代的方式解决。
我们知道斐波那契数的前2个数都1,然后前2个数相加就是第3个数,那么我们从前往后,从小到大计算就行了。这样就有下面的代码:
#nclude<stdio.h>
int main() {
int i = 3;
int num = 0;
int input = 0;
int first = 1;
int second = 1;
printf("请输入想知道的第n个斐波那契数列: \n");//1 1 2 3 5
scanf("%d", &input);
while (i <= input) {
num = first + second;
first = second;
second = num;
i++;
}
if (input > 2) {
printf("第%d个斐波那契数是%d\n", input, num);
}
else {
num = 1;
printf("第%d个斐波那契数是%d\n", input, num);
}
return 0;
}
迭代的方式去实现这个代码,效率就要高出很多了。有时候,递归虽好,但是也会引入⼀些问题,所以我们⼀定不要迷恋递归,凡事都把握个度,适可而止就好。
三.使用递归和迭代的时机
1.如果使用递归写代码,非常容易,写出的代码没问题,那就需要递归。
2.如果使用递归写出的问题,是存在明显的缺陷,拿就不能使用递归,得用迭代的方式出来。
总结
以上就是今天要讲的内容,本文仅仅简单介绍了何为递归,为什么用递归,怎么用递归;同时将递归与迭代两者进行比较,可谓各有千秋,该用谁时取决于时机是否之前。