3.4 递归函数
1 递归概念
俄罗斯套娃比喻
递归就像打开一个俄罗斯套娃,每个娃娃里面都有一个更小的娃娃,直到最小的那个不能再打开为止。
void openMatryoshka(int size) {if(size == 1) { // 基本情况 - 最小的娃娃cout << "打开最小的娃娃!" << endl;return;}cout << "打开大小为" << size << "的娃娃" << endl;openMatryoshka(size - 1); // 打开里面的小娃娃
}
递归三要素
- 基本情况(Base Case):递归的终止条件
- 递归关系(Recursive Relation):问题与子问题的关系
- 向基本情况靠近:每次递归调用都应更接近基本情况
递归与循环的比较
特性 | 递归 | 循环 |
---|---|---|
实现方式 | 函数调用自身 | 重复执行代码块 |
内存消耗 | 需要栈空间保存每次调用状态 | 只需要固定内存 |
代码可读性 | 对某些问题更直观 | 通常更直接 |
适用问题 | 树形结构、分治问题 | 线性重复操作 |
2 阶乘函数
数学定义
n! = n × (n-1) × … × 1
0! = 1 (特殊情况)
递归实现
int factorial(int n) {// 基本情况if(n == 0 || n == 1) {return 1;}// 递归关系:n! = n * (n-1)!return n * factorial(n - 1);
}
调用过程可视化
factorial(4)
= 4 * factorial(3)
= 4 * (3 * factorial(2))
= 4 * (3 * (2 * factorial(1)))
= 4 * (3 * (2 * 1))
= 4 * (3 * 2)
= 4 * 6
= 24
3 斐波那契数列
数列定义
F(0) = 0
F(1) = 1
F(n) = F(n-1) + F(n-2) (n ≥ 2)
递归解法
int fibonacci(int n) {if(n == 0) return 0;if(n == 1) return 1;return fibonacci(n-1) + fibonacci(n-2);
}
效率问题讨论
递归解法存在重复计算问题。例如计算fib(5):
fib(5)
= fib(4) + fib(3)
= (fib(3) + fib(2)) + (fib(2) + fib(1))
= ... // fib(2)被计算了多次
优化方案:使用记忆化(Memoization)存储已计算结果
int fibMemo(int n, vector<int>& memo) {if(n <= 1) return n;if(memo[n] != -1) return memo[n];memo[n] = fibMemo(n-1, memo) + fibMemo(n-2, memo);return memo[n];
}int fibonacci(int n) {vector<int> memo(n+1, -1);return fibMemo(n, memo);
}
4 递归练习
汉诺塔问题
void hanoi(int n, char from, char to, char aux) {if(n == 1) {cout << "将盘子1从" << from << "移动到" << to << endl;return;}hanoi(n-1, from, aux, to);cout << "将盘子" << n << "从" << from << "移动到" << to << endl;hanoi(n-1, aux, to, from);
}// 调用示例:hanoi(3, 'A', 'C', 'B');
十进制转二进制
void decToBinary(int n) {if(n == 0) return;decToBinary(n / 2);cout << n % 2;
}// 调用示例:decToBinary(10); 输出1010
最大公约数(欧几里得算法)
int gcd(int a, int b) {if(b == 0) return a;return gcd(b, a % b);
}// 调用示例:gcd(48, 18)返回6
递归调试技巧
- 添加打印语句:跟踪递归调用
int factorial(int n, int depth = 0) {cout << string(depth, ' ') << "计算factorial(" << n << ")" << endl;if(n == 0) return 1;int result = n * factorial(n-1, depth+1);cout << string(depth, ' ') << "返回" << result << endl;return result;
}
- 绘制调用树:在纸上画出递归过程
- 使用调试器:观察调用栈的变化
常见递归错误
- 缺少基本情况:导致无限递归
// 错误示例!
int factorial(int n) {return n * factorial(n-1); // 没有终止条件!
}
- 不向基本情况靠近:
// 错误示例!
int badRecursion(int n) {if(n == 0) return 1;return badRecursion(n); // 永远不会结束!
}
- 栈溢出:递归太深导致调用栈溢出
递归与迭代转换
任何递归算法都可以转换为迭代形式(使用栈):
// 迭代版阶乘
int factorialIter(int n) {int result = 1;for(int i = 1; i <= n; i++) {result *= i;}return result;
}
本章综合练习
- 基础题:实现递归函数计算数字各位之和,如sumDigits(123)=6
- 提高题:使用递归判断字符串是否是回文
- 挑战题:实现递归的二分查找算法
- 思考题:分析递归计算斐波那契数列的时间复杂度
编程项目:实现一个递归的文件系统扫描器,输出指定目录下的所有文件树形结构(伪代码):
void printFileTree(string path, int depth = 0) {// 打印当前目录/文件(带缩进)// 如果是目录:// 获取所有子项// 对每个子项递归调用printFileTree
}
性能优化建议
- 对于深度递归问题,考虑使用迭代解法
- 使用记忆化(Memoization)优化重复计算
- 尾递归优化(某些编译器支持)
- 限制递归深度(设置最大深度)