有人可能会问:什么叫递归?大家不用觉得很难,其实递归就是一种解决问题的方法,在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);
}
迭代的⽅式去实现这个代码,效率就要⾼出很多了。 有时候,递归虽好,但是也会引⼊⼀些问题,所以我们⼀定不要迷恋递归,适可⽽⽌就好。