文章目录
0、总结
1、什么是递归
递归:在C语言中,递归就是函数自己调用自己。
例1:最简单的C语言递归代码:
#include <stdio.h>
int main()
{
printf("hehe\n");
main();
return 0;
}
上述代码仅演示递归基本形式,存在死递归问题,会导致栈溢出(Stack overflow)。
1.1 递归的思想
递归是将大问题转化为相似但规模较小的子问题求解,直至子问题无法再拆分。所以递归的思考方式是把大事化小的过程。递归中的递是递推的意思,归是回归的意思。
1.2 递归的限制条件
递归书写需满足两个必要条件:
- 递归存在限制条件,当满足这个限制条件的时候,递归停止。
- 每次递归调用之后越来越接近这个限制条件。
2、递归的举例
2.1 求n的阶乘
前置知识:
一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。
自然数n的阶乘写作n!。
题目:
计算n的阶乘(不考虑溢出),n的阶乘就是1~n的数字累积相乘。
2.1.1 分析和代码实现
n的阶乘的公式:n! = n * (n - 1)!
举例:
5! = 5*4*3*2*1
4! = 4*3*2*1
所以:
5! = 5*4!
这种思路属于将大问题转化为相似但规模较小的问题来求解。
当n == 0
的时候,n的阶乘是1,其余n的阶乘都可以通过公式计算。
n的阶乘的递归公式如下:
我们可以定义函数Fact
来求n的阶乘,其中Fact(n)
表示求n的阶乘,而Fact(n-1)
则表示求n-1的阶乘。
代码如下:
#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);
int ret = Fact(n);
printf("%d\n", ret);
return 0;
}
2.1.2 画图推演
2.2 顺序打印一个整数的每一位
题目:
输入一个整数m,打印这个按照顺序打印整数的每一位。
比如:
输入:1234 输出:1 2 3 4
输入:520 输出:5 2 0
2.2.1 分析和代码实现
首先思考的是,如何得到这个数的每一位?
如果n是1位数,n的每一位就是n自己。如果n是超过1位数,就需要拆分每一位。
1、从给定的数字
n
开始,不断地执行以下两步操作,直到n
变为0:
使用%10
操作获取n
的最低位数字。
使用/10
操作去掉n
的最低位数字。
2、但这个问题是,得到的数字顺序是原始数字的逆序。
例如,对于数字1234:
- 首先,
1234 % 10
得到4,1234 / 10
得到123。- 然后,
123 % 10
得到3,123 / 10
得到12。- 接着,
12 % 10
得到2,12 / 10
得到1。- 最后,
1 % 10
得到1,1 / 10
得到0,结束循环。- 所以,得到的数字序列是4, 3, 2, 1,这是1234的逆序。
我们发现一个数字的最低位是最容易得到的,通过%10
就能得到。
那我们写一个函数Print
来打印一个整数的每一位数字,从最高位到最低位依次打印。
例如,对于数字12345:
- 首先,因为
12345 > 9
,Print(12345)
会调用Print(1234)
。- 然后,因为
1234 > 9
,Print(1234)
会调用Print(123)
。- 接着,因为
123 > 9
,Print(123)
会调用Print(12)
。- 因为
12 > 9
,Print(12)
会调用Print(1)
,1 <= 9
是递归的终止条件,Print(1)
打印1
。- 然后,控制返回到
Print(12)
,打印2
。- 控制继续返回,依次打印
3
、4
、5
。- 最终输出是:
1 2 3 4 5
。
总结是:
Print(12345) + printf(5)
==> Print(1234) + printf(4)
==> Print(123) + printf(3)
==> Print(12) + printf(2)
==> Print(1) + printf(1)
代码如下:
#include <stdio.h>
void Print(int n)
{
if (n > 9)
Print(n / 10);
printf("%d ", n % 10);
}
int main()
{
int m = 0;
scanf("%d", &m);
Print(m);
return 0;
}
2.2.2 画图推演
2.3 总结
1、通过前两个举例,会感受到写一个代码,虽然递归难以想到,但使用递归写出的代码会非常简单。往往一个代码使用递归可能就几行代码。如果把递归写成非递归(迭代)的方式,就需要十几行甚至几十行代码。
2、如果递归的不恰当书写,会导致一些无法接受的后果,那我们还是应该放弃使用递归,使用迭代解决问题。
3、后期学习数据结构的时候,经常会使用递归。
4、在面试(笔试)的时候,你用递归很快的解决了代码问题,遇到一些苛求的面试官,就会让你改成非递归。
3、递归与迭代
在C语言中,每次函数调用都会在栈区为其分配一块内存空间,称为栈帧,用于保存函数调用的局部变量。递归调用时,每层递归都会占用新的栈帧空间,直到递归结束才逐层释放。递归层次过深会导致栈帧空间消耗过多,可能引发栈溢出。为避免递归,可采用迭代(循环)的方式实现相同功能。
例如:计算n的阶乘。
int Fact(int n)
{
int i = 0;
int ret = 1;
for (i = 1; i <= n; i++)
{
ret *= i;
}
return ret;
}
上述代码是能够完成任务,并且效率是比递归的方式更好。
事实上,许多问题倾向于用递归形式解释,因为递归比非递归更清晰。然而,这些问题的迭代实现通常效率更高。当问题极其复杂,难以用迭代实现时,递归的简洁性可以弥补其运行时的开销。
3.1 求第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;
}
当我们n输入为50的时候,需要很长时间才能算出结果,这个计算所花费的时间,是我们很难接受的,这也说明递归的写法是非常低效的,那是为什么呢?
通过图片很容易发现,递归的过程中会有重复计算,递归层次越深,冗余计算就会越多。我们可以测试第3个斐波那契数列被重复计算多少次。如下:
#include <stdio.h>
int count = 0;
int Fib(int n)
{
if (n == 3)
// 统计第3个斐波那契数列被计算的次数
count++;
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);
printf("\ncount = %d\n", count);
return 0;
}
可以发现,在计算第40个斐波那契数列的时候,使用递归的方式,第3个斐波那契数列就被重复计算了39088169次,这些计算是非常冗余的。因此使用递归是不明智的,需要通过迭代的方式去解决。
斐波那契数列的前两个数都是1,且从第三个数开始,每个数都是前两个数的和。因此,我们可以从前往后,按照从小到大的顺序依次计算每个数。代码如下:
#include <stdio.h>
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 1;
while (n >= 3)
{
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;
}
采用迭代的方式去实现这个代码,效率会有显著提升。尽管递归在某些情况下很实用,但它也可能带来一些问题。因此,我们不应过度依赖递归,适可而止就好。
4、拓展
4.1 青蛙跳台阶问题
题目:
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
数据范围:
1≤𝑛≤40 1≤n≤40
要求:时间复杂度:𝑂(𝑛),空间复杂度: 𝑂(1)
这个题有递归(自上而下)和迭代(自下而上)两种解法。
4.1.1 分析
按照笨方法进行统计,会发现:
没有台阶:f(0) = 1,青蛙跳到第0台阶的跳法只有一种,那就是不跳。
一个台阶:f(1) = 1,青蛙跳到第1台阶的跳法只有一种,那就是从第0级台阶直接跳到第1级台阶。
两个台阶:f(2) = 2
三个台阶:f(3) = 3
四个台阶:f(4) = 5
通过观察可以归纳得出f(n) = f(n-1) + f(n-2)
这个公式,那么为什么这个公式经得起推敲呢?
我们可以这样的思考(自下而上思考):
- 假设有4个台阶,青蛙每次可以选择跳1级或2级。
- 如果青蛙首先跳1级到达第一个台阶,那么它还剩下3个台阶要跳。此时,以第一个台阶为新的起点,青蛙面对剩下的3个台阶,就有3种不同的跳跃方法可以到达顶端。
- 另一方面,如果青蛙首先跳2级到达第二个台阶,那么它还剩下2个台阶。此时,以第二个台阶为新的起点,青蛙面对剩下的2个台阶,有2种不同的跳跃方法。
- 因此,青蛙跳上4个台阶总共有2+3=5种方法。
可以这样思考,(自上而下思考):
- 青蛙跳到第n级台阶的所有可能方式。这些方式可以分为两类。
- 青蛙最后一步是从第
n-1
级台阶直接跳1级上来的。 - 青蛙最后一步是从第
n-2
级台阶直接跳2级上来的。
- 通过观察图像,我们可以很自然地得出结论:最后一步的实现是第一种方式与第二种方式的累加。
- 注意的是,这两类方式是互斥的,即青蛙不可能同时从第
n-1
级和第n-2
级跳到第n
级。因此,青蛙跳到第n
级台阶的总方法数就是这两类方法数之和。
4.1.2 代码
递归(自上而下)会重复计算,而迭代(自下而上),则可以在计算的过程中保存“跳到较低台阶的跳法数量”的计算结果。
在迭代过程中,可以通过先计算f(2) = f(1) + f(0),接着计算f(3) = f(2) + f(1),以此类推直到f(n)。在此过程中,每次计算都至少保存最后两个结果。由于这种方法利用了上一步的计算结果来快速得到下一步的结果,因此它体现了动态规划的思想。动态规划的核心就是利用已解决的子问题的解来构造原问题的解,从而避免重复计算,提高效率。
递归:
#include <stdio.h>
//递归
int Jump(int n)
{
if (n <= 2)
return n;
else
return Jump(n - 1) + Jump(n - 2);
}
int main(void)
{
int n = 0;
printf("请输入台阶的数量:");
scanf("%d", &n);
for (int i = 0; i < n; ++i)
{
printf("跳到第%d个台阶时有%d种跳法\n", i + 1, Jump(i + 1));
}
return 0;
}
运行结果:
迭代(动态规划):
#include <stdio.h>
//迭代
int Jump2(int n)
{
int a = 1;
int b = 2;
int c = 1;
int t = n;
while (n > 2)
{
c = a + b;
a = b;
b = c;
n--;
}
if (t <= 2)
return n;
else
return c;
}
int main(void)
{
int n = 0;
printf("请输入台阶的数量:");
scanf("%d", &n);
for (int i = 0; i < n; ++i)
{
printf("跳到第%d个台阶时有%d种跳法\n", i + 1, Jump2(i + 1));
}
return 0;
}
运行结果:
4.2 汉诺塔问题
#include <stdio.h>
int count = 0;
void move(int n, char x, char y)
{
count++;
printf("第%d次:%d号圆盘:%c->%c\n", count, n, x, y);
}
void Hanoi(int n, char a, char b, char c)
{
if (n == 1)
{
move(n, a, c);
}
else
{
Hanoi(n - 1, a, c, b);
move(n, a, c);
Hanoi(n - 1, b, a, c);
}
}
int main(void)
{
int n = 0;
int a = 'A';
int b = 'B';
int c = 'C';
printf("玩汉诺塔,请输入圆盘的个数:");
scanf("%d", &n);
Hanoi(n, a, b, c);
printf("把A上的圆盘都移动到C上,总共移动了%d次", count);
return 0;
}
运行结果:
4.3 尾递归求斐波那契数
尾递归:比线性递归多几个参数,这个参数是上一次调用函数得到的结果。
所以,关键在于,尾递归每次调用都在收集结果,避免了线性递归不收集结果只能依次展开消耗内存的坏处。
代码:
#include <stdio.h>
int count = 0;
// 使用尾递归
long long fib_tail(int n, long long a, long long b) {
if (n == 3)
count++;
if (n == 0) return a;
if (n == 1) return b;
return fib_tail(n - 1, b, a + b);
}
int main() {
int n = 0;
scanf("%d", &n);
// n:要计算的斐波那契数列的项数。
// a:斐波那契数列的第n-1项。
// b:斐波那契数列的第n项。
long long ret = fib_tail(n, 0, 1);
printf("Fibonacci number at position %d is %lld\n", n, ret);
printf("count = %d\n", count);
return 0;
}
函数调用过程:
1、fib_tail(3,0,1)->fib_tail(2,1,1)
2、fib_tail(2,1,1)->fib_tail(1,1,2)
3、fib_tail(1,1,2)->返回2
运行结果:
完。