在学习递归之前,先来学习以下什么是函数。
C++函数的基本概念
函数是C++程序的基本构建块,用于封装可重复使用的代码逻辑。函数通过接受输入参数(可选)、执行特定任务并返回结果(可选)来简化代码结构,提高可维护性。
// 函数定义示例
int add(int a, int b) {
return a + b;
}
函数的组成
- 返回类型:指定函数返回的数据类型(如
int
、void
)。 - 函数名:标识函数的唯一名称(如
add
)。 - 参数列表:输入参数及其类型(如
int a, int b
),可为空。 - 函数体:包含实际执行逻辑的代码块。
void printMessage() {
std::cout << "Hello, World!" << std::endl;
}
函数的调用
函数通过函数名和实际参数调用,返回值可赋给变量或直接使用。
int result = add(3, 5); // 调用add函数
printMessage(); // 调用无参函数
参数传递方式
- 传值(By Value):传递参数的副本,原值不受影响。
- 传引用(By Reference):直接操作原变量,需在参数前加
&
。 - 传指针(By Pointer):通过指针间接访问变量。
// 传引用示例
void swap(int &x, int &y) {
int temp = x;
x = y;
y = temp;
}
函数重载
C++允许同名函数通过不同的参数列表(类型或数量)实现重载。编译器根据调用时的参数匹配具体函数。
int max(int a, int b) { return a > b ? a : b; }
double max(double a, double b) { return a > b ? a : b; }
默认参数
函数参数可设置默认值,调用时若省略则使用默认值。默认参数必须从右向左连续定义。
void log(string message, int level = 1) {
cout << "[" << level << "] " << message << endl;
}
// 调用:log("Error") 或 log("Warning", 2)
递归函数
函数可以直接或间接调用自身,适用于分治类问题(如阶乘、斐波那契数列)。需注意终止条件以避免无限递归。
int factorial(int n) {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
内联函数
通过inline
关键字建议编译器将函数体直接插入调用处,减少函数调用的开销。适用于简单且频繁调用的函数。
inline double square(double x) { return x * x; }
Lambda表达式(C++11)
匿名函数,常用于简化短逻辑或作为回调函数。语法为[capture](parameters) -> return_type { body }
。
auto sum = [](int a, int b) { return a + b; };
cout << sum(2, 3); // 输出5
接下来,我们进入正题————递归
什么是递归
递归是一种函数调用自身的编程技术。它通过将复杂问题分解为更小的相似子问题来解决。递归通常包含两个关键部分:基线条件(递归终止条件)和递归条件(调用自身的条件)。
void recursiveFunction() {
// 基线条件
if (baseCaseCondition) {
return;
}
// 递归条件
recursiveFunction();
}
递归的基本结构
递归函数必须有一个明确的终止条件,否则会导致无限递归和栈溢出。典型的递归结构包括:
- 基线条件:最简单的情况,直接返回结果
- 递归步骤:将问题分解为更小的子问题,调用自身处理
int factorial(int n) {
// 基线条件
if (n == 0 || n == 1) {
return 1;
}
// 递归步骤
return n * factorial(n - 1);
}
递归的工作原理
每次递归调用都会在内存栈中创建一个新的栈帧,包含函数的参数和局部变量。当满足基线条件时,递归开始"回退",逐层返回计算结果。
以计算阶乘为例:
- factorial(3)调用factorial(2)
- factorial(2)调用factorial(1)
- factorial(1)返回1
- factorial(2)返回2×1=2
- factorial(3)返回3×2=6
递归的优缺点
优点:
- 代码简洁易读,特别是对于数学定义明确的问题
- 天然适合处理树形结构和分治算法
- 逻辑表达清晰,接近问题的数学描述
缺点:
- 可能产生大量函数调用,消耗栈空间
- 重复计算可能导致效率低下
- 调试可能比迭代更困难
常见递归示例
斐波那契数列:
int fibonacci(int n) {
if (n <= 1) {
return n;
}
return fibonacci(n-1) + fibonacci(n-2);
}
二分查找:
int binarySearch(int arr[], int left, int right, int target) {
if (right >= left) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) return mid;
if (arr[mid] > target) return binarySearch(arr, left, mid-1, target);
return binarySearch(arr, mid+1, right, target);
}
return -1;
}
递归与迭代的转换
任何递归算法都可以用迭代实现,反之亦然。选择哪种方式取决于问题的特性和可读性要求。
递归阶乘的迭代版本:
int factorial(int n) {
int result = 1;
for (int i = 1; i <= n; ++i) {
result *= i;
}
return result;
}
尾递归优化
尾递归是递归的一种特殊形式,递归调用是函数的最后操作。某些编译器可以优化尾递归,避免额外的栈开销。
尾递归阶乘:
int factorialTail(int n, int accumulator = 1) {
if (n == 0) return accumulator;
return factorialTail(n - 1, n * accumulator);
}
递归的应用场景
递归特别适合以下场景:
- 数学定义递归的问题(阶乘、斐波那契数列)
- 树形结构遍历(二叉树、文件系统)
- 分治算法(快速排序、归并排序)
- 回溯算法(迷宫求解、N皇后问题)
递归的注意事项
使用递归时需要考虑:
- 确保存在明确的基线条件
- 递归深度不能过大,以免栈溢出
- 避免重复计算(可用备忘录技术优化)
- 某些语言对递归深度有限制
// 备忘录优化的斐波那契
int fibMemo(int n, std::unordered_map<int, int>& memo) {
if (n <= 1) return n;
if (memo.find(n) != memo.end()) return memo[n];
memo[n] = fibMemo(n-1, memo) + fibMemo(n-2, memo);
return memo[n];
}