目录
一、什么是递归
在数学中,递归是一种定义函数或数列的方法,其中一个或多个初始项被明确给出,而后续的项则通过前一项或几项来定义。这种方法通常用于定义那些不能或不容易直接表达为封闭形式的序列或函数。递归定义的关键特征是它引用自身,即后续的项或值依赖于先前的项或值。
递归定义通常包括两个部分:
1. 基本情况(Base Case):这是递归定义的起点,它明确给出数列或函数的一个或几个初始值。基本情况确保递归有明确的起点,并且可以避免无限递归。
2. 递归关系(Recursive Relation):这是定义数列或函数后续项的方法。它描述了如何从前一项或几项推导出下一项。递归关系确保数列或函数的每个后续值都可以通过先前的值来计算。
递归的一个经典例子是斐波那契数列。
斐波那契数列的定义是:
- 基本情况:F(0) = 0, F(1) = 1
- 递归关系:对于所有 n > 1,F(n) = F(n-1) + F(n-2)
这意味着斐波那契数列的前两项是直接给出的,而后续的每一项都是前两项之和。
例如,F(2) = F(1) + F(0) = 1 + 0 = 1,F(3) = F(2) + F(1) = 1 + 1 = 2,依此类推。
递归在数学中非常普遍,它不仅用于定义数列,还用于定义函数、集合、操作等。递归方法在数学的许多领域都有应用,包括组合数学、数论、计算理论等。通过递归,数学家能够简洁地定义和操作那些复杂或无限的结构。
这有跟C语言中的函数递归有什么关系呢?
在数学中,递归是一种定义序列或函数的方法,其中一个或多个初始项被明确给出,而后续的项则通过前一项或几项来定义。这个概念可以直接映射到编程中的函数递归。
在编程中,特别是C语言中,函数递归是指一个函数直接或间接地调用自身。这与数学递归的定义有直接联系,因为它们都基于“使用先前的结果来计算后续结果”的原则。
让我们以斐波那契数列为例,这是一个在数学中经常用递归定义的序列。
在C语言中,我们可以将这个数学递归定义转换为一个函数递归:
int fibonacci(int n)
{
if (n <= 1)
{
return n; // 基本情况
}
else
{
return fibonacci(n - 1) + fibonacci(n - 2); // 递归情况
}
}
在这个函数中:
基本情况:当 `n` 小于或等于 1 时,函数直接返回 `n`。这对应于数学递归中的基本情况。
递归情况:当 `n` 大于 1 时,函数调用自身来计算 `fibonacci(n - 1)` 和 `fibonacci(n - 2)`。这对应于数学递归中的递归关系。
这样,我们就将数学递归的概念直接转换为了C语言中的函数递归。通过这种方式,编程中的函数递归可以看作是数学递归在计算机科学中的应用。
如此一来就能够大大减少我们计算的压力,但是“压力不会凭空消失,它只会从一个物体转移到另一个物体”
所谓递归,就是递推和回归。
在使用递归的时候,我们会遇到一个问题,就是递推到什么时候,才能回归呢?
递归在书写的时候,其实是有2个必要条件的:
• 递归存在限制条件,当满⾜这个限制条件的时候,递归便不再继续。
• 每次递归调⽤之后越来越接近这个限制条件。
如果一个递推中不满足这个限制条件,就会导致,递推的深度过大,直接给系统的CPU给“干烧了”,然后就出现了栈溢出的情况。因此对于一个函数在递推的时候,我们需要做的就是明确递推的逻辑,以此减少栈溢出的情况。
二、几个递推的举例
案例一:求n的阶乘
#include <stdio.h>
long long factorial(int n)
{
if (n <= 1)
{
return 1; // 基本情况
}
else
{
return n * factorial(n - 1); // 递归情况
}
}
int main()
{
int n;
printf("请输入一个正整数:");
scanf("%d", &n);
if (n < 0)
{
printf("阶乘仅对非负整数定义。\n");
}
else
{
printf("%d 的阶乘是 %lld\n", n, factorial(n));
}
return 0;
}
在这个程序中:
- 程序首先提示用户输入一个正整数。
- 使用
scanf
函数读取用户输入的数,并将其存储在变量n
中。 - 程序检查输入的数是否为非负整数。阶乘通常只对非负整数定义,因此如果输入是负数,程序会输出一个错误消息。
- 如果输入是非负整数,程序调用
factorial
函数来计算阶乘,并输出结果。
相比较与循环来说,使用循环来计算阶乘是一个更高效的方法,尤其是对于较大的数值,因为它避免了递归带来的大量函数调用和栈空间的使用。
#include <stdio.h>
long long factorial(int n)
{
long long result = 1;
for (int i = 1; i <= n; i++)
{
result *= i; // 逐步计算阶乘
}
return result;
}
int main()
{
int n;
printf("请输入一个正整数:");
scanf("%d", &n);
if (n < 0)
{
printf("阶乘仅对非负整数定义。\n");
}
else
{
printf("%d 的阶乘是 %lld\n", n, factorial(n));
}
return 0;
}
在这个程序中:
factorial
函数通过一个for
循环来计算阶乘。它从 1 开始,逐步乘以每个整数,直到n
。result
变量用于存储中间结果,并在每次循环迭代中更新。main
函数读取用户输入,并调用factorial
函数来计算和显示阶乘。
这种方法在处理大数值时通常比递归更有效,因为它不需要额外的函数调用开销,且在大多数情况下,栈空间的使用也更少。然而,对于非常大的数值,即使使用循环,也可能遇到整数溢出的问题。在这种情况下,可能需要使用特殊的数据类型(如 long long
)来处理大数。
案例二:输⼊⼀个整数m,按照顺序打印整数的每⼀位
考虑实际问题,我们可以设计一个递归函数,让函数每次处理整数的一部分,并递归地调用自身来处理剩下的部分。具体步骤如下:
- 基本情况:当整数变为 0 时,递归终止。
- 递归情况:在每次递归调用中,我们打印整数的最后一位数字,然后去除这一位,递归地调用函数来处理剩下的数字。
#include <stdio.h>
void printDigits(int m)
{
if (m == 0)
{ // 基本情况
return;
}
else
{
printf("%d ", m % 10); // 打印最后一位
printDigits(m / 10); // 递归调用,处理剩下的数字
}
}
int main()
{
int m;
printf("请输入一个整数:");
scanf("%d", &m);
printf("整数的每一位(从最后一位开始)是:");
printDigits(m);
printf("\n");
return 0;
}
在这个程序中:
printDigits
函数是递归函数,用于按顺序打印整数的每一位。- 在
main
函数中,程序首先提示用户输入一个整数,然后调用printDigits
函数来打印它的每一位。
这个程序使用递归简洁地实现了按顺序打印整数每一位的功能。递归的基本情况是当整数变为 0 时停止递归,递归情况是打印当前整数的最后一位,并递归地调用自身来处理剩下的数字。
当我们使用循环的时候,代码就是这样子实现的
#include <stdio.h>
void printDigits(int m)
{
if (m == 0)
{
printf("0"); // 特殊情况:输入为0
return;
}
while (m > 0)
{
printf("%d ", m % 10); // 打印最后一位
m /= 10; // 移除最后一位
}
printf("\n"); // 打印换行符
}
int main()
{
int m;
printf("请输入一个整数:");
scanf("%d", &m);
printf("整数的每一位(从最后一位开始)是:");
printDigits(m);
return 0;
}
案例三:求第n个斐波那契数
使用递归时
#include <stdio.h>
long long fibonacci(int n) 、
{
if (n <= 1)
{
return n; // 基本情况
}
else
{
return fibonacci(n - 1) + fibonacci(n - 2); // 递归情况
}
}
int main()
{
int n;
printf("请输入一个正整数:");
scanf("%d", &n);
printf("斐波那契数列的第 %d 个数是 %lld\n", n, fibonacci(n));
return 0;
}
递归方法虽然简洁,但效率不高,尤其是对于较大的 n 值,因为它涉及大量的重复计算。我们在解放双手,减轻压力的时候,其实是把压力转移到电脑上,让它去帮我们计算这庞大的数据。
迭代方法使用循环来计算斐波那契数列,避免了重复计算,因此效率更高。下面是一个使用迭代的 C 语言程序示例:
#include <stdio.h>
long long fibonacci(int n)
{
long long a = 0, b = 1, temp;
if (n <= 1)
{
return n;
}
for (int i = 2; i <= n; i++)
{
temp = a + b;
a = b;
b = temp;
}
return b;
}
int main()
{
int n;
printf("请输入一个正整数:");
scanf("%d", &n);
printf("斐波那契数列的第 %d 个数是 %lld\n", n, fibonacci(n));
return 0;
}
在这个迭代方法中,我们使用两个变量 a
和 b
来存储斐波那契数列中的前两个数,并在每次循环迭代中更新这两个变量。这种方法避免了递归方法中的重复计算,因此对于较大的 n 值更加高效。
三、使用时的注意事项
正确地应用递归可以帮助我们更清晰地解决问题,但在某些情况下,迭代可能是更好的选择。以下是一些在使用递归时应注意的事项:
-
定义清晰的基本情况:递归函数必须有一个或多个基本情况,这是递归终止的条件。确保这些基本情况能够被正确识别和执行,以避免无限递归。
-
递归关系:确保递归调用正确地表达了问题的子问题。递归关系应该能够逐步将问题分解为更小的、可解决的子问题。
-
效率考虑:递归可能会导致大量的重复计算,特别是在计算斐波那契数列等场景中。在某些情况下,使用动态规划或迭代方法可以显著提高效率。
-
栈空间:递归会使用栈空间来存储函数调用的状态。对于非常深的递归调用(例如,非常大的递归深度),可能会导致栈溢出。在这种情况下,迭代可能是更好的选择。
-
参数和返回值:确保递归函数的参数和返回值正确无误。参数应该准确地传递给子问题,而返回值应该正确地反映问题的解。
-
代码可读性和维护性:虽然递归可以简化某些问题的解决方案,但过于复杂的递归可能会降低代码的可读性和维护性。确保递归逻辑清晰且易于理解。
-
测试和调试:递归函数可能比迭代函数更难以测试和调试。确保对递归函数进行充分的测试,以验证其正确性。
-
替代方案:在决定使用递归之前,考虑是否有更适合的替代方案,如迭代、动态规划或其他算法。
-
性能分析:对递归函数进行性能分析,特别是在处理大量数据或需要高效率的应用中。如果递归导致性能瓶颈,考虑优化递归算法或转换为迭代。
通过注意这些事项,我们可以更有效地使用递归,并在必要时选择迭代作为替代方案。在实际应用中,选择递归还是迭代,取决于具体问题的性质、效率要求以及代码的可维护性。