目录
一.什么是递归
在C语言中,递归就是自己调用自己.
以下是一个C语言的递归代码
#include<stdio.h>
int main()
{
printf("hello world!\n");
main();
return 0;
}
在打印完hello world之后,又重新执行main函数,又继续打印hello world无限的执行下去.
但是这串代码最终会陷入死循环,导致栈溢出.(stack overflow)
1.递归的思想
把一个大型复杂的问题,不断的转化为更小的同类型的问题,直到不能继续转化下去,递归就结束.
也就是大事化小的思想.
递归:递是指递推的意思,归是指回归的意思.
二.递归的限制条件
2.递归的限制条件
1.递归存在限制的条件,当满足这个条件的时候,递归不会再进行下去.
2.递归总是越来越接近这个条件.
三.递归举例
1.举例1:求n的阶乘.
一个正整数的阶乘(factorial)就是所有小于以及等于这个数的正整数的乘积,并且规定0的阶乘为1;
阶乘符号: !
例:5! = 5*4*3*2*1 4! = 4*3*2*1
由以上的条件我们知道n! = n(n-1)!
5! = 5 * 4!
这样我们就可以把一个大问题简化为一个与原问题相识的,但规模较小的问题来求解.
如果Fact(n)指的是n的阶乘,那么我们很容易知道n的阶乘的递归公式如下:
代码:
#include<stdio.h>
int Fact(int n)
{
if(n==0)
return 1;
else
return n * Fact(n - 1);
}
int main()
{
int n = 0;
scanf("%d",&n);
printf("%d\n",Fact(n));
return 0;
}
在进行测试的时候,n的值输入的不要太大,否则可能会溢出.
2.举例2:顺序打印一个整数的每一位.
比如:
输入:1234 输出 1 2 3 4
输入987 输出 9 8 7
如果是倒序打印每一位,我们首先可能想到的是,通过%10就可以得到最后一位,再/10,就可以去掉最后一位,通过反复进行我们就可以倒叙打印每一位了.
但是如果是顺序打印就不一样了,
我们发现一个数字的最低位很容易得到,%10就可以了,那么我们可以定义一个函数Print().
Print(n)
Print(1234) = Print(123) + 4
Print(123) = Print(12) + 3
.......
这样我们就可以成功顺序打印每一位了.
代码:
#include<stdio.h>
void Print(int n)
{
if (n < 10)
{
printf("%d ", n);
}
else
{
Print(n / 10);
printf("%d ", n % 10);
}
}
int main()
{
int n = 0;
scanf("%d",&n);
Print(n);
return 0;
}
在这个过程中,我们就是使用了大事化小的思路
把Print(1234)打印1234每一位,拆解成Print(123)打印123每一位,再打印4;
把Print(123)打印123每一位,拆解成Print(12)打印12每一位,再打印3.
把Pirnt(12)打印12每一位,拆解成Print(1)打印1每一位,再打印2.
而Print(1)就是打印n.
四.递归与迭代
递归是一种很好的编程技巧,但是可能会被误用,看到推导的公式,就很容易写成递归的形式
在C语言中,每一次函数的调用,都要为本次函数调用在栈区申请一块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行堆栈,或者函数栈帧.
函数如果不返回的话.函数对应的栈帧空间就会一直被占用.所以如果函数调用中存在递归调用的话,每一次递归函数调用就会开辟属于自己的栈帧空间,直到函数递归不再进行下去,才会逐层释放空间.所以如果函数递归的方式完成代码,递归的层次太深,就会浪费太多的栈帧空间,引起栈溢出的问题,也就是开头说写的代码的问题.
比如计算n的阶乘.
以下代码也可以完成问题
int Fact(int n)
{
int i = 0;
int ret = 1;
for(i = 1; i <= n ;i++)
{
ret *= i;
}
return ret;
}
上述代码是能够完成问题的,而且效率也比函数递归的方式更高.
事实上,我们看到的许多问题是以递归的方式解决问题的,这仅仅是因为它比非递归的方式理解起来更加容易,这是这类问题通过非递归也就是迭代的方式实现往往比递归的方式更高效.
3.举例3:求第n个斐波那契数.
首先我们先了解斐波那契数列(Fibonacci):
斐波那契数列: 1 1 2 3 5 8 13 21 .
也就是在规定前两个数是1的情况下,其中某一个数是前两个数之和.
Fibo(n) = Fibo(n-1)+FIbo(n-2)
看到这个推导公式,很容易将代码写成递归的形式
#include<stdio.h>
int Fibo(int n)
{
if (n <= 2)
{
return 1;
}
else
{
return Fibo(n - 1) + Fibo(n - 2);
}
}
int main()
{
int n = 0;
scanf("%d",&n);
printf("%d\n",Fibo(n));
return 0;
}
在我们对这个代码进行测试的时候,我们发现当n的值稍微有点大的时候,程序运行所需的时间很长.
在计算机如此快的计算速度下,我们仍然需要很长的时间,只能说明这个代码的效率太低了.
在递归的过程中,我们很容易发现有许多重复的计算(图中相同的颜色),递归的次数越多,这些计算就会冗余,也就是效率降低.比如我们可以尝试计算第3个斐波那契数被计算的次数
#include<stdio.h>
int count = 0;
int Fibo(int n)
{
if (n == 3)
count++;
if (n <= 2)
{
return 1;
}
else
{
return Fibo(n - 1) + Fibo(n - 2);
}
}
int main()
{
int n = 0;
scanf("%d",&n);
printf("%d\n",Fibo(n));
printf("%d\n",count);
return 0;
}
count就是次数.
仅仅是求第50个斐波那契数,第三个斐波那契数就被计算了五亿多次,可见程序运行如此复杂,在我测试这个代码的时候大概用了20多秒.
所以我们就需要使用其他方法,我们可以将三个数依次定义为 a b c,c就等于a+b,当进行完这一过程后,我们将b赋值给a,再将c赋值给a,这样就往后移动了一步,再继续相加,就可以得到后面一个数.
#include<stdio.h>
int main()
{
int n = 0;
int a = 1;
int b = 1;
int c = 1;
scanf("%d", &n);
if (n <= 2)
{
printf("%d\n",c);
}
for (int i = 1; i <= n-2; i++)
//开始计算的时候,是从第三个数开始的,所以需要-2
{
c = a + b;
a = b;
b = c;
}
printf("%d\n",c);
return 0;
}
这样的方式效果就比递归的方式高出很多.