递归算法:自己调用自己的神奇魔法
一、递归是什么?
形象比喻:俄罗斯套娃
打开一个娃娃,里面有个小一点的娃娃,再打开,里面更小的...直到最小的那个。
递归就是:函数自己调用自己
二、递归三要素(最重要!)
- 基线条件:什么时候停止?(最小的套娃)
- 递归条件:如何缩小问题?(打开当前套娃)
- 递归调用:调用自己(处理里面的套娃)
三、示例1:阶乘(Factorial)
什么是阶乘?
5! = 5 × 4 × 3 × 2 × 1 = 120
4! = 4 × 3 × 2 × 1 = 24
3! = 3 × 2 × 1 = 6
2! = 2 × 1 = 2
1! = 1
0! = 1 (规定)
递归思想:
要算 5!
= 5 × 4!
= 5 × 4 × 3!
= 5 × 4 × 3 × 2!
= 5 × 4 × 3 × 2 × 1!
= 5 × 4 × 3 × 2 × 1
= 120
C#代码(带详细注释):
using System;
class RecursionExample1
{
static void Main()
{
Console.WriteLine("=== 递归计算阶乘 ===\n");
// 测试几个阶乘
TestFactorial(0, "0! 应该是 1(规定)");
TestFactorial(1, "1! 应该是 1");
TestFactorial(2, "2! 应该是 2");
TestFactorial(3, "3! 应该是 6");
TestFactorial(4, "4! 应该是 24");
TestFactorial(5, "5! 应该是 120");
// 让用户自己试
Console.Write("\n你想算几的阶乘?输入数字:");
if (int.TryParse(Console.ReadLine(), out int n) && n >= 0)
{
int result = Factorial(n);
Console.WriteLine($"{n}! = {result}");
}
}
// 递归计算阶乘
static int Factorial(int n)
{
Console.WriteLine($"调用 Factorial({n})");
// 1. 基线条件:什么时候停止?
if (n == 0 || n == 1) // 0! = 1, 1! = 1
{
Console.WriteLine($" 到达基线条件:Factorial({n}) = 1");
return 1; // 停止递归
}
// 2. 递归条件:如何缩小问题?
// n! = n × (n-1)!
int smaller = n - 1;
Console.WriteLine($" {n}! = {n} × {smaller}!");
// 3. 递归调用:自己调用自己
int resultOfSmaller = Factorial(smaller);
// 得到小问题的结果后,计算当前问题的结果
int result = n * resultOfSmaller;
Console.WriteLine($" 计算完成:{n} × {resultOfSmaller} = {result}");
return result;
}
static void TestFactorial(int n, string description)
{
Console.WriteLine($"\n{description}");
Console.WriteLine($"{"".PadRight(30, '-')}");
int result = Factorial(n);
Console.WriteLine($"结果:{result}");
}
}
输出结果(计算5!的过程):
调用 Factorial(5)
5! = 5 × 4!
调用 Factorial(4)
4! = 4 × 3!
调用 Factorial(3)
3! = 3 × 2!
调用 Factorial(2)
2! = 2 × 1!
调用 Factorial(1)
到达基线条件:Factorial(1) = 1
计算完成:2 × 1 = 2
计算完成:3 × 2 = 6
计算完成:4 × 6 = 24
计算完成:5 × 24 = 120
结果:120
四、示例2:斐波那契数列(Fibonacci)
什么是斐波那契数列?
第0项:0
第1项:1
第2项:1(0+1)
第3项:2(1+1)
第4项:3(1+2)
第5项:5(2+3)
第6项:8(3+5)
第7项:13(5+8)
...
规律:F(n) = F(n-1) + F(n-2)
递归思想:
要算 F(5)
= F(4) + F(3)
= [F(3) + F(2)] + [F(2) + F(1)]
= ... 一直分解到 F(1) 和 F(0)
C#代码(带详细注释):
using System;
class RecursionExample2
{
static void Main()
{
Console.WriteLine("=== 递归计算斐波那契数列 ===\n");
// 斐波那契数列前10项
Console.WriteLine("斐波那契数列:");
Console.WriteLine("位置:0 1 2 3 4 5 6 7 8 9 10");
Console.WriteLine("数值:0 1 1 2 3 5 8 13 21 34 55");
Console.WriteLine("\n计算过程演示:\n");
// 计算 F(5) 并显示过程
int n = 5;
int result = FibonacciWithTrace(n, "");
Console.WriteLine($"\nF({n}) = {result}");
// 计算前几个数
Console.WriteLine("\n完整数列:");
for (int i = 0; i <= 10; i++)
{
Console.Write($"{Fibonacci(i)} ");
}
}
// 递归计算斐波那契数(带调用跟踪)
static int FibonacciWithTrace(int n, string indent)
{
Console.WriteLine($"{indent}调用 Fibonacci({n})");
// 1. 基线条件:什么时候停止?
if (n == 0)
{
Console.WriteLine($"{indent} 基线条件:F(0) = 0");
return 0;
}
if (n == 1)
{
Console.WriteLine($"{indent} 基线条件:F(1) = 1");
return 1;
}
// 2. 递归条件:如何缩小问题?
// F(n) = F(n-1) + F(n-2)
Console.WriteLine($"{indent} F({n}) = F({n-1}) + F({n-2})");
// 3. 递归调用:自己调用自己(两次!)
int left = FibonacciWithTrace(n - 1, indent + " ");
int right = FibonacciWithTrace(n - 2, indent + " ");
int result = left + right;
Console.WriteLine($"{indent} 计算结果:F({n}) = {left} + {right} = {result}");
return result;
}
// 普通的递归斐波那契函数
static int Fibonacci(int n)
{
if (n == 0) return 0;
if (n == 1) return 1;
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
}
输出结果(计算F(5)的过程):
调用 Fibonacci(5)
F(5) = F(4) + F(3)
调用 Fibonacci(4)
F(4) = F(3) + F(2)
调用 Fibonacci(3)
F(3) = F(2) + F(1)
调用 Fibonacci(2)
F(2) = F(1) + F(0)
调用 Fibonacci(1)
基线条件:F(1) = 1
调用 Fibonacci(0)
基线条件:F(0) = 0
计算结果:F(2) = 1 + 0 = 1
调用 Fibonacci(1)
基线条件:F(1) = 1
计算结果:F(3) = 1 + 1 = 2
调用 Fibonacci(2)
F(2) = F(1) + F(0)
调用 Fibonacci(1)
基线条件:F(1) = 1
调用 Fibonacci(0)
基线条件:F(0) = 0
计算结果:F(2) = 1 + 0 = 1
计算结果:F(4) = 2 + 1 = 3
调用 Fibonacci(3)
F(3) = F(2) + F(1)
调用 Fibonacci(2)
F(2) = F(1) + F(0)
调用 Fibonacci(1)
基线条件:F(1) = 1
调用 Fibonacci(0)
基线条件:F(0) = 0
计算结果:F(2) = 1 + 0 = 1
调用 Fibonacci(1)
基线条件:F(1) = 1
计算结果:F(3) = 1 + 1 = 2
计算结果:F(5) = 3 + 2 = 5
F(5) = 5
五、递归的优缺点
优点:
- 代码简洁:几行代码解决复杂问题
- 思路清晰:符合人类的思维方式
- 解决特定问题:适合树、图等结构
缺点:
- 可能很慢:如斐波那契的递归版本
- 可能栈溢出:递归太深会报错
- 重复计算:如F(3)在斐波那契中被计算多次
六、递归vs循环
斐波那契的循环版本(更快):
static int FibonacciLoop(int n)
{
if (n == 0) return 0;
if (n == 1) return 1;
int prev2 = 0; // F(0)
int prev1 = 1; // F(1)
int current = 0;
for (int i = 2; i <= n; i++)
{
current = prev1 + prev2; // F(i) = F(i-1) + F(i-2)
prev2 = prev1; // 更新F(i-2)
prev1 = current; // 更新F(i-1)
}
return current;
}
对比:
- 递归F(40):可能需要几秒(重复计算多)
- 循环F(40):瞬间完成(只计算一次)
七、递归算法基础练习题
练习题1:理解递归调用过程
题目:写出下面递归函数计算 Sum(5) 的完整过程
int Sum(int n)
{
if (n == 1) return 1; // 基线条件
return n + Sum(n - 1); // 递归调用
}
手动计算过程:
Sum(5)
= 5 + Sum(4)
= 5 + (4 + Sum(3))
= 5 + 4 + (3 + Sum(2))
= 5 + 4 + 3 + (2 + Sum(1))
= 5 + 4 + 3 + 2 + 1
= 15
答案:Sum(5) = 15
调用栈图示:
Sum(5)
↓
5 + Sum(4)
↓
4 + Sum(3)
↓
3 + Sum(2)
↓
2 + Sum(1)
↓
1 ← 基线条件
练习题2:递归函数填空
题目:补全递归打印数字的代码
void PrintNumbers(int n)
{
if (______) // 填空1:基线条件
{
return;
}
Console.Write(n + " "); // 先打印当前数字
______; // 填空2:递归调用
}
// 调用 PrintNumbers(5) 应该输出:5 4 3 2 1
答案:
// 填空1:n <= 0 或 n == 0
// 填空2:PrintNumbers(n - 1)
// 如果要输出 1 2 3 4 5,代码应该是:
void PrintNumbersAscending(int n)
{
if (n <= 0) return;
PrintNumbersAscending(n - 1); // 先递归
Console.Write(n + " "); // 再打印
}
八、综合练习题
练习题3:汉诺塔问题(经典递归)
问题描述: 有三根柱子A、B、C。A柱上有n个盘子,从小到大叠放。要把所有盘子移动到C柱,要求:
- 每次只能移动一个盘子
- 大盘子不能在小盘子上面
- 可以借助B柱
递归思路:
移动n个盘子从A到C:
1. 移动n-1个盘子从A到B(借助C)
2. 移动第n个盘子从A到C
3. 移动n-1个盘子从B到C(借助A)
C#代码框架:
void Hanoi(int n, char from, char to, char aux)
{
if (n == 1) // 基线条件:只有一个盘子
{
Console.WriteLine($"移动盘子1从{from}到{to}");
return;
}
// 递归步骤
Hanoi(n - 1, from, aux, to); // 填空1
Console.WriteLine($"移动盘子{n}从{from}到{to}");
Hanoi(n - 1, aux, to, from); // 填空2
}
// 测试:3个盘子
Hanoi(3, 'A', 'C', 'B');
输出结果:
移动盘子1从A到C
移动盘子2从A到B
移动盘子1从C到B
移动盘子3从A到C
移动盘子1从B到A
移动盘子2从B到C
移动盘子1从A到C
练习题4:递归求最大公约数(GCD)
题目:用递归实现辗转相除法求最大公约数
int GCD(int a, int b)
{
if (______) // 填空1
{
return a;
}
return GCD(______, ______); // 填空2
}
// 测试:GCD(48, 18) = 6
// 因为:48 ÷ 18 = 2 余 12
// 18 ÷ 12 = 1 余 6
// 12 ÷ 6 = 2 余 0
答案:
// 填空1:b == 0
// 填空2:b, a % b
// 完整代码:
int GCD(int a, int b)
{
if (b == 0) return a;
return GCD(b, a % b);
}
九、递归常见错误
错误1:没有基线条件(无限递归)
// ❌ 错误:会永远递归下去
int BadRecursion(int n)
{
return n + BadRecursion(n - 1); // 没有停止条件!
}
// 结果:栈溢出异常(StackOverflowException)
错误2:基线条件不对
// ❌ 错误:基线条件永远达不到
int BadFactorial(int n)
{
if (n == 0) return 1; // 基线条件
return n * BadFactorial(n + 1); // 应该减1,不是加1!
}
// 调用 BadFactorial(5) → 无限递归
正确写法:
// ✅ 正确
int GoodFactorial(int n)
{
if (n == 0 || n == 1) // 正确的基线条件
return 1;
return n * GoodFactorial(n - 1); // 正确的递归调用
}
十、学习建议
- 先理解三要素:基线条件、递归条件、递归调用
- 画调用图:在纸上画出递归调用过程
- 从小开始:先理解Factorial(3),再理解更大的
- 对比学习:对比递归和循环的实现
- 注意栈深度:不要递归太深(通常不要超过1000层)
十一、一句话总结
递归 = 基线条件(何时停) + 递归调用(调自己)
- 阶乘递归:n! = n × (n-1)!
- 斐波那契递归:F(n) = F(n-1) + F(n-2)
- 关键:问题要能分解成更小的相同问题
记住:写递归函数时,先想基线条件,不然会无限递归!