04 函数与递归
GESP 4-5 级CSP-J 核心
级别标注:GESP 4 级(函数)/ 5 级(递归)(考级要求)/ CSP-J 核心(竞赛进阶,递归是搜索与分治的地基)。
一、函数的基本定义与调用
C++ 实例
#include <iostream>
using namespace std;
// 求两数最大公约数(辗转相除法:gcd(a,b) = gcd(b, a%b))
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b); // 余数为 0 时返回除数
}
// 返回 int 类型的函数:两数相加
int add(int x, int y) {
return x + y; // 把计算结果返回给调用处
}
// void 表示无返回值,只执行操作(这里打印一行横线)
void printLine(int n) {
for (int i = 0; i < n; i++) cout << "-"; // 连续输出 n 个 '-'
cout << endl; // 换行
}
int main() {
cout << add(3, 4) << endl; // 调用 add,输出 7
cout << gcd(12, 18) << endl; // 输出 6
printLine(20); // 打印 20 个横线
return 0; // 正常结束
}函数四要素:返回类型、函数名、参数列表、函数体。
形象理解函数像餐厅的标准化菜谱:
int add(int x, int y) 就是“收两份食材 x、y,按固定步骤加工,端出一份 int 菜”。void 表示“只负责干活不上菜”(无返回值)。考级视角GESP 4 级要求掌握:函数定义/调用/返回值、形参与实参、局部变量与全局变量。读程序题常考“形参改名后实参变不变”——默认值传递,改形参不影响实参。
二、参数传递方式
2.1 值传递(默认):拷贝一份,不影响原变量
C++ 实例
#include <iostream>
using namespace std;
// 值传递:形参 x 是实参的"复印件"
void f(int x) {
x = 100; // 只修改复印件,不影响外面的变量
}
int main() {
int a = 1;
f(a); // 传入的是 a 的拷贝
cout << a << endl; // 仍为 1(原件未被修改)
return 0;
}2.2 引用传递(&):直接操作原变量
C++ 实例
#include <iostream>
using namespace std;
// 引用传递:形参 &x、&y 是实参的"别名",直接操作原件
void swap2(int &x, int &y) {
int t = x; // 先把 x 的值存入临时变量
x = y; // x 变成 y
y = t; // y 变成原来的 x
}
int main() {
int a = 3, b = 5;
swap2(a, b); // 引用参数会真实交换 a、b
cout << a << " " << b << endl; // 输出 5 3
return 0;
}函数模板注意事项:函数定义必须出现在调用前,或先用"函数声明/原型":
C++ 实例
#include <iostream>
#include <algorithm>
using namespace std;
int max3(int, int, int); // 函数原型(声明):先告诉编译器"存在这个函数"
int main() {
cout << max3(1, 9, 4) << endl; // 调用时编译器已知其签名
return 0;
}
// 定义可以放在 main 之后(只要之前有声明即可)
int max3(int a, int b, int c) {
return max(a, max(b, c)); // 先取 b、c 较大者,再与 a 比较
}形象理解值传递像复印身份证:函数拿到复印件随便改,原件不受影响;引用传递(
&)像把原件直接递进去,函数一改原件就变。想交换两个变量的值,必须用引用或指针。易错点交换两个数用
void swap2(int &x, int &y);如果漏写 &,交换只在函数内生效,调用后 a、b 原封不动——这是新手写排序代码时最常见的“换了等于没换”。三、函数重载(同名不同参)
C++ 实例
#include <iostream>
using namespace std;
// 函数重载:同名函数,靠参数类型区分调用哪个版本
int sum(int a, int b) { return a + b; } // int 版本
double sum(double a, double b) { return a + b; } // double 版本
int main() {
cout << sum(1, 2) << endl; // 传 int → 调用 int 版 → 3
cout << sum(1.5, 2.5) << endl; // 传 double → 调用 double 版 → 4
return 0;
}形象理解函数重载像同名的多合一工具:
sum(1, 2) 自动选 int 版本,sum(1.5, 2.5) 自动选 double 版本——编译器根据参数类型“认人下菜”。易错点重载只认参数类型和个数,不认返回值类型:
int f() 和 double f() 同时存在会编译报错。也不要写参数能互相隐式转换的暧昧重载,容易调用歧义。四、递归基础
递归 = 函数调用自身。必须满足: 1. 终止条件(基准情形,否则无限递归爆栈)。 2. 规模递减(每步向终止条件靠近)。
4.1 阶乘
C++ 实例
// 递归求阶乘:n! = n × (n-1)!
long long fact(int n) {
if (n <= 1) return 1; // 基准情形:0! = 1! = 1
return n * fact(n - 1); // 递归调用:规模每次减 1
}
// 执行过程:fact(5) = 5 * fact(4) = 5*4*3*2*1 = 1204.2 斐波那契(朴素递归,用于理解递归,竞赛会优化)
C++ 实例
// 朴素递归斐波那契:第 n 项 = 前两项之和
long long fib(int n) {
if (n <= 2) return 1; // 基准:fib(1)=fib(2)=1
return fib(n - 1) + fib(n - 2); // 拆成两个更小的子问题
}
// 注意:朴素写法会重复计算大量子问题,n 大时极慢(竞赛用记忆化/DP,见 15 章)⚠️ 朴素
fib(45)会非常慢(指数级重复计算),竞赛中需用"记忆化"或 DP(详见 15 章)。
4.3 递归执行过程的理解(以 fact(4) 为例)
示例
fact(4)
-> 4 * fact(3)
-> 3 * fact(2)
-> 2 * fact(1) = 2
= 3 * 2 = 6
= 4 * 6 = 24
理解要点:先"递下去"(不断压栈),到基准后"归回来"(依次出栈计算并返回)。
形象理解递归像俄罗斯套娃:打开最大的娃娃,里面是稍小的娃娃,直到最小的那个“终止娃娃”。每次打开都在“递”,打开到底再一层层“归”回来组装答案。没有终止娃娃就是无限套娃(爆栈)。
考级视角GESP 5 级考递归求阶乘、斐波那契、汉诺塔;要求能手推递归执行过程。建议用“压栈/出栈”画图法:每一层调用就是往栈顶压一层,返回值时再弹出来。
五、递归经典应用
5.1 汉诺塔
C++ 实例
#include <iostream>
using namespace std;
// 汉诺塔:把 n 个盘子从 from 柱借助 aux 柱移到 to 柱
void hanoi(int n, char from, char to, char aux) {
if (n == 1) { // 只剩一个盘子:直接移动
cout << from << " -> " << to << endl;
return;
}
// 三步走:先把上面 n-1 个挪开,再移最大盘
hanoi(n - 1, from, aux, to); // 1) 把 n-1 个小盘 from→aux
cout << from << " -> " << to << endl; // 2) 最大盘 from→to
hanoi(n - 1, aux, to, from); // 3) 把 n-1 个小盘 aux→to
}
int main() {
hanoi(3, 'A', 'C', 'B'); // 3 个盘子从 A 移到 C,B 作辅助
return 0;
}5.2 指数计算(快速幂见 16 章)
C++ 实例
// 普通递归求 a^n:a^n = a × a^(n-1)
long long power(int a, int n) {
if (n == 0) return 1; // 基准:任何数的 0 次方为 1
return a * power(a, n - 1); // 指数每次减 1,共递归 n 层
}
// 进阶:快速幂用"指数折半"把层数降到 O(log n),见 16 章5.3 全排列(回溯,详见 14 章)
C++ 实例
#include <iostream>
using namespace std;
const int N = 10;
int n, a[N]; // a 存当前排列(正在构造的结果)
bool used[N]; // used[i] 标记数字 i 是否已被使用
void perm(int k) { // 正在填第 k 个位置
if (k == n) { // 所有位置填满:输出一个排列
for (int i = 0; i < n; i++) cout << a[i] << " ";
cout << endl;
return;
}
for (int i = 1; i <= n; i++) { // 尝试把 1~n 中未用的数字放到第 k 位
if (!used[i]) {
used[i] = true; // 进入前标记:占用数字 i
a[k] = i; // 第 k 位放 i
perm(k + 1); // 递归填下一位
used[i] = false; // 返回后撤销标记(回溯)
}
}
}
int main() {
cin >> n;
perm(0); // 从第 0 位开始填
return 0;
}形象理解汉诺塔像搬家工人:想搬 n 个箱子,先请人把上面 n-1 个搬到临时位置,自己搬最重的大箱子,再请人把 n-1 个搬回目标位。自己只搬一个,其余交给“递归分身”。
易错点全排列回溯最容易漏掉“撤销标记”:
used[i] = false; 必须写,否则下一层分支会看到被污染的标记,输出排列不全。口诀:标记 → 递归 → 撤销。六、易错点 / 常见坑点
- ⚠️ 忘记终止条件 → 栈溢出
stack overflow/ 死递归。 - ⚠️ 递归层数过深(如超过 10 万层)可能爆栈,C++ 默认栈空间有限,竞赛大递归常用"迭代/循环"或加大栈空间。
- ⚠️ 递归+全局变量要小心"进入递归修改、回溯忘记还原"。
- ⚠️ 值传递与引用传递混淆:想在函数里改外层变量必须用
&。 - ⚠️ 局部变量出了函数就销毁,别返回局部数组/局部 string 的指针(undefined behavior)。
- ⚠️ 函数声明顺序:调用前必须"见过"声明或定义。
七、考级要求 vs 竞赛进阶
- 考级要求:函数定义/调用/返回值、形参实参、作用域(GESP 4 级);递归求阶乘、斐波那契、汉诺塔(GESP 5 级)。
- 竞赛进阶(CSP-J/NOIP):递归是DFS、分治、回溯、归并排序的共同底座;要求理解调用栈、会用全局数组代替大量局部拷贝、重构大递归。
八、课内练习建议
- 用递归求 1+2+...+n。
- 递归求解最大公约数(辗转相除)。
- 递归判断回文串。
- 递归求 n 个数的最大值。
- 理解并手写一遍汉诺塔的移动过程(n=3 共 7 步)。