函数递归
目录
✨ 引言:
对于 C 语言学习者来说,递归就像 “编程界的魔术”—— 它能用几行简洁的代码解决复杂问题,却又因 “看不见的调用栈” 让新手望而却步。很多人能看懂递归代码,却想不通它的执行流程;能写出简单递归,却踩坑栈溢出或效率低下。
一、什么是递归?(通俗理解:自己调用自己)
1.1 递归的本质:分而治之
递归是一种 “大事化小” 的解决问题思想 :
将一个复杂的大问题,分解为一个或多个与原问题结构相同但规模更小的子问题,直到子问题小到能直接解决(基准情况),再通过子问题的解反向推导原问题的解。
在 C 语言中,递归的具体表现是:函数直接或间接调用自身。
1.2 错误示范:无限递归(栈溢出警告!)
#include <stdio.h>
int main()
{
printf("hehe\n");
main(); // 函数调用自身,无终止条件
return 0;
}
1.3 栈溢出的底层原因(必懂!)
每一次函数调用,系统都会在栈(Stack) 上分配一块 “函数栈帧”,用于存储:
- 函数的参数;
- 局部变量;
- 返回地址(函数执行完后要回到的位置)。
栈的容量是有限的(通常几 MB),上面的代码中,main 函数无限调用自身,栈帧会像 “叠积木” 一样持续累积,直到栈空间被耗尽,触发 栈溢出 (Stack Overflow) 错误,程序直接崩溃。
1.4 递归的两个核心条件(缺一不可!)
✅ 存在基准情况(Base Case):
当问题规模缩小到基准情况时,递归停止,直接返回明确结果(比如 0! = 1);
✅ 递推逼近基准:
每次递归调用必须让问题规模 “变小”,朝着基准情况推进(比如 n! 分解为 n * (n-1)!)。
生动类比:俄罗斯套娃
- 递推:一层一层打开外层套娃,直到找到最里面不能再打开的小娃娃(基准情况);
- 回归:找到小娃娃后,再一层一层合上套娃,最终回到最初的外层套娃(原问题的解)。
二、递归经典示例(图解执行流程)
示例 1:求 n 的阶乘(递归入门必练)
问题分析
- 数学定义:
n! = n * (n-1) * (n-2) * ... * 1; - 递归拆解:
n! = n * (n-1)!(大问题→小问题); - 基准情况:
0! = 1、1! = 1(小问题直接解)。
递归实现
#include <stdio.h>
// 用unsigned int避免负数输入导致无限递归
int Fact(unsigned int n)
{
if (n == 0) // 基准情况:0! = 1
{
return 1;
}
else// 递推:n! = n * (n-1)!
{
return n * Fact(n - 1);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fact(n);
printf("%d\n", ret);
return 0;
}
执行流程图解(以Fact(4)为例)
Fact(4)
|
|-- 判断 n == 0? 4 != 0
|-- 执行 return 4 * Fact(3)
| |
| --> Fact(3)
| |
| |-- 判断 n == 0? 3 != 0
| |-- 执行 return 3 * Fact(2)
| | |
| | --> Fact(2)
| | |
| | |-- 判断 n == 0? 2 != 0
| | |-- 执行 return 2 * Fact(1)
| | | |
| | | --> Fact(1)
| | | |
| | | |-- 判断 n == 0? 1 != 0
| | | |-- 执行 return 1 * Fact(0)
| | | | |
| | | | --> Fact(0)
| | | | |
| | | | |-- 判断 n == 0? 0 == 0 <-- 触发基准情况
| | | | |-- 执行 return 1
| | | | |
| | | | <-- 返回 1
| | | |-- 计算 1 * 1 = 1,执行 return 1
| | | |
| | | <-- 返回 1
| | |-- 计算 2 * 1 = 2,执行 return 2
| | |
| | <-- 返回 2
| |-- 计算 3 * 2 = 6,执行 return 6
| |
| <-- 返回 6
|-- 计算 4 * 6 = 24,执行 return 24
核心流程总结
- 递推阶段:从
Fact(4)逐层分解到Fact(0),问题规模不断缩小; - 回归阶段:从
Fact(0)的结果1逐层反向计算,最终得到Fact(4)=24。
示例 2:顺序打印整数的每一位(理解 “先递后归”)
需求
输入一个整数(如1234),按高位到低位顺序打印每一位(输出1 2 3 4)。
问题拆解
- 要打印
1234:先打印123的每一位,再打印4(1234%10); - 要打印
123:先打印12的每一位,再打印3(123%10); - 基准情况:当数字为个位数(
n<10)时,直接打印该数字。
递归实现(已优化)
#include <stdio.h>
void Print(int n)// 以1234为例
{
if (n > 9) // 递推条件:不是个位数,先处理高位
{
Print(n / 10); // 1234/10=123,递归处理高位
}
// 基准情况+回归操作:打印当前最低位
printf("%d ", n % 10);
}
int main()
{
int n = 0;
scanf("%d", &n);
Print(n); // 打印n的每一位
return 0;
}
执行流程图解(以Print(1234)为例)
Print(1234)
|
|-- 判断 n > 9? 1234 > 9 → 是
|-- 执行 Print(1234 / 10) = Print(123)
| |
| --> Print(123)
| |
| |-- 判断 n > 9? 123 > 9 → 是
| |-- 执行 Print(123 / 10) = Print(12)
| | |
| | --> Print(12)
| | |
| | |-- 判断 n > 9? 12 > 9 → 是
| | |-- 执行 Print(12 / 10) = Print(1)
| | | |
| | | --> Print(1)
| | | |
| | | |-- 判断 n > 9? 1 > 9 → 否(触发基准情况)
| | | |-- 执行 printf("%d ", 1 % 10) → 打印 "1 "
| | | |
| | | <-- 返回
| | |-- 执行 printf("%d ", 12 % 10) → 打印 "2 "
| | |
| | <-- 返回
| |-- 执行 printf("%d ", 123 % 10) → 打印 "3 "
| |
| <-- 返回
|-- 执行 printf("%d ", 1234 % 10) → 打印 "4 "
核心亮点
打印操作发生在回归阶段:递推时只分解问题,不执行打印;回归时才逐层打印最低位,最终实现 “高位到低位” 的顺序输出 —— 这正是递归 “先递后归” 的精髓!
三、递归与迭代:该怎么选?
迭代是通过for、while等循环语句重复执行代码,是与递归并列的 “重复执行” 逻辑。两者各有优劣,需根据场景灵活选择。
1. 阶乘的迭代实现(修正原代码笔误)
#include <stdio.h>
int Fact(int n)
{
int i = 0;
int ret = 1;
for (i = 1; i <= n; i++) // 从1开始累积相乘
{
ret *= i;
}
return ret;
}
int main()
{
int n = 0;
// 原笔误:scanf("%d ,&n");(格式字符串与变量地址未分离)
scanf("%d", &n); // 修正后:格式字符串与变量地址分开
int ret = Fact(n);
printf("%d\n", ret);
return 0;
}
2. 斐波那契数列:暴露递归的致命缺陷
斐波那契数列定义:F(1)=1,F(2)=1,n>2时F(n)=F(n-1)+F(n-2)。这是递归的经典案例,但朴素递归存在严重的效率问题。
朴素递归实现(低效!)
#include <stdio.h>
int Fib(int n)
{
if (n <= 2)
{
return 1;
}
else
{
return Fib(n - 1) + Fib(n - 2);
}
}
// 带计数的版本:展示重复计算问题
int count = 0;
int Fib_count(int n)
{
if (n == 3) // 统计Fib(3)的调用次数
{
count++;
}
if (n <= 2)
{
return 1;
}
else
{
return Fib_count(n - 1) + Fib_count(n - 2);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
// 演示重复计算:n=40时,Fib(3)调用次数极多
count = 0;
Fib_count(40);
printf("计算Fib(40)时,Fib(3)被调用了 %d 次\n", count);
return 0;
}
效率问题:重复计算的 “灾难”
以Fib(5)为例,递归调用树如下:
Fib(5)
/ \
Fib(4) Fib(3) // Fib(3)第1次调用
/ \ / \
Fib(3) Fib(2) Fib(2) Fib(1) // Fib(3)第2次调用
/ \
Fib(2) Fib(1)
- 计算
Fib(5)时,Fib(3)被重复调用 2 次,Fib(2)被重复调用 3 次; - 当
n=40时,Fib(3)被调用的次数会达到数百万次 —— 重复计算导致时间复杂度呈指数级增长(O(2ⁿ)),程序运行极慢。
高效迭代实现(修正返回值缺陷)
#include <stdio.h>
int Fib(int n)
{
if (n <= 2) // 基准情况:n=1或2时返回1
return 1;
int a = 1; // 存储F(n-2)
int b = 1; // 存储F(n-1)
int c = 0; // 存储F(n) = F(n-1)+F(n-2)
while (n > 2)
{
c = a + b;
a = b; // 更新F(n-2)为上一轮的F(n-1)
b = c; // 更新F(n-1)为上一轮的F(n)
n--;
}
return c; // 原代码缺少返回值,修正后返回F(n)
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
递归与迭代核心对比表
| 特性 | 递归 | 迭代(循环) |
|---|---|---|
| 代码简洁性 | 简洁优雅,逻辑直观 | 相对繁琐,逻辑需手动梳理 |
| 执行效率 | 低(函数调用开销 + 重复计算) | 高(无调用开销,无重复计算) |
| 内存占用 | 高(栈帧累积,有栈溢出风险) | 低(固定变量,内存稳定) |
| 调试难度 | 高(调用栈嵌套,流程复杂) | 低(循环流程清晰,易跟踪) |
四、递归的适用场景与避坑指南
1. 何时优先用递归?
✅ 问题是递归定义的(如阶乘、斐波那契数列);
✅ 问题可自然分解为相似子问题(如归并排序、快速排序);
✅ 操作对象是递归数据结构(如链表、树、图的深度优先搜索)。
2. 何时避免用递归?
❌ 问题规模极大(可能导致栈溢出);
❌ 朴素递归存在严重重复计算(如斐波那契数列);
❌ 对性能要求极高,且迭代解法同样简洁。
3. 递归避坑 3 大要点
⚠️ 牢记基准条件:缺少基准条件会导致无限递归和栈溢出;
⚠️ 确保递推逼近基准:比如Fact(n)不能写成Fact(n+1),否则永远达不到基准;
⚠️ 警惕重复计算:遇到重复计算问题,优先用迭代或 “记忆化搜索” 优化。
五、拓展思考题(练手必备)
- 青蛙跳台阶问题:一只青蛙一次可以跳 1 级或 2 级台阶,求跳上 n 级台阶的总方法数?(提示:本质是斐波那契数列)
- 汉诺塔问题:将 A 杆上的 N 个圆盘(上小下大)通过 B 杆移到 C 杆,每次只能移一个圆盘,且大盘不能叠在小盘上。请打印移动步骤?(提示:递归拆解为 “移 N-1 个盘→移第 N 个盘→移 N-1 个盘”)
📝 总结
递归的核心是 “找准基准条件 + 正确递推分解”:递推是 “分解问题”,回归是 “计算结果”。它不是 “炫技工具”,而是解决特定问题的高效思路 —— 用好了简洁优雅,用不好则效率低下、栈溢出。
如果这篇博客帮你理清了递归逻辑,欢迎点赞收藏!
124

被折叠的 条评论
为什么被折叠?



