CSharp(十九) 方法递归详解
1. 什么是递归
递归(Recursion) 是指在方法的定义中调用方法自身的编程技巧。
通俗理解:一个方法自己调用自己,就像两面镜子对在一起产生的无限映像。但递归不是无限循环,它必须有终止条件,否则会导致栈溢出。
生活中的递归类比
- 俄罗斯套娃:打开一个娃娃,里面还有一个更小的娃娃,直到最小的那个无法再打开。
- 文件的文件夹结构:文件夹里包含文件和子文件夹,子文件夹里又包含文件和子文件夹。
- 数学归纳法:先证明基本情况成立,再证明如果 n 成立则 n+1 成立。
2. 递归的两大要素
任何一个正确的递归方法,必须包含两个部分:
2.1 递归终止条件(Base Case / 基线条件)
递归不能无限进行,必须有一个或几个明确的、不再继续递归的条件。
当满足终止条件时,方法直接返回结果,不再调用自身。这是递归的"出口"。
2.2 递归表达式(Recursive Case / 递归条件)
将大问题分解为一个或多个规模更小的同类子问题。
方法通过调用自身来解决子问题,最终逐步逼近终止条件。
图解
递归方法:
┌─────────────────────────────────┐
│ 返回类型 方法名(参数) │
│ { │
│ if (终止条件) │ ← 基线条件(出口)
│ return 简单结果; │
│ │
│ // 递归条件 │ ← 缩小问题规模
│ return 方法名(更小的参数); │
│ } │
└─────────────────────────────────┘
3. 递归的执行原理:调用栈
3.1 什么是调用栈(Call Stack)
C# 程序运行时会维护一个调用栈,用于跟踪方法的调用关系。每次调用一个方法,都会在栈上压入一个栈帧(Stack Frame),包含:
- 方法的参数
- 局部变量
- 返回地址
当方法执行完毕后,栈帧被弹出(Pop),程序返回到调用点继续执行。
3.2 递归的栈过程
以计算 Factorial(3) 为例:
调用 Factorial(3)
│
▼
┌──────────────────────────────────────────┐
│ 栈帧: Factorial(3) │
│ n = 3, 等待 Factorial(2) 的结果 │
│ └─→ 调用 Factorial(2) │
│ ┌──────────────────────────────┐ │
│ │ 栈帧: Factorial(2) │ │
│ │ n = 2, 等待 Factorial(1) │ │
│ │ └─→ 调用 Factorial(1) │ │
│ │ ┌──────────────────┐ │ │
│ │ │ 栈帧: Factorial(1)│ │ │
│ │ │ n = 1, 到达终止 │ │ │
│ │ │ return 1 ────────┼───┤ │ ← 开始"回溯"
│ │ └──────────────────┘ │ │
│ │ 收到 1, return 2 × 1 = 2 ───┼───┤ │
│ └──────────────────────────────┘ │
│ 收到 2, return 3 × 2 = 6 ────────────────┤
└──────────────────────────────────────────┘
最终返回 6
关键理解:递归分为两个阶段:
- 递推(Forward Phase):不断调用自身,问题规模逐步缩小,向终止条件靠近。
- 回溯(Backward Phase):到达终止条件后,逐层返回结果,每层利用子问题的结果计算本层结果。
4. 基础示例
4.1 计算阶乘(Factorial)
阶乘定义:
n! = n × (n-1) × (n-2) × ... × 2 × 1,特别地,0! = 1。
递归定义:n! = n × (n-1)!,且0! = 1。
using System;
class Program
{
/// <summary>
/// 递归计算 n 的阶乘
/// </summary>
/// <param name="n">非负整数</param>
/// <returns>n 的阶乘值</returns>
static long Factorial(int n)
{
// 终止条件(基线条件):0! = 1
if (n == 0)
{
return 1;
}
// 递归条件:n! = n × (n-1)!
return n * Factorial(n - 1);
}
static void Main()
{
Console.WriteLine(Factorial(0)); // 输出: 1
Console.WriteLine(Factorial(1)); // 输出: 1
Console.WriteLine(Factorial(5)); // 输出: 120
Console.WriteLine(Factorial(10)); // 输出: 3628800
}
}
执行追踪(Factorial(3)):
Factorial(3) → 3 * Factorial(2)
→ 3 * (2 * Factorial(1))
→ 3 * (2 * (1 * Factorial(0)))
→ 3 * (2 * (1 * 1))
→ 3 * (2 * 1)
→ 3 * 2
→ 6
4.2 斐波那契数列(Fibonacci)
斐波那契数列:
F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2)(n ≥ 2)
数列:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
using System;
class Program
{
/// <summary>
/// 递归计算第 n 个斐波那契数
/// </summary>
/// <param name="n">索引(从0开始)</param>
/// <returns>第 n 个斐波那契数</returns>
static long Fibonacci(int n)
{
// 终止条件
if (n == 0) return 0;
if (n == 1) return 1;
// 递归条件:F(n) = F(n-1) + F(n-2)
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
static void Main()
{
for (int i = 0; i <= 10; i++)
{
Console.WriteLine($"F({i}) = {Fibonacci(i)}");
}
// 输出:
// F(0) = 0
// F(1) = 1
// F(2) = 1
// F(3) = 2
// F(4) = 3
// F(5) = 5
// F(6) = 8
// F(7) = 13
// F(8) = 21
// F(9) = 34
// F(10) = 55
}
}
注意:这个朴素递归实现的时间复杂度是 O(2^n),效率很低。n 稍大就会非常慢。后面会介绍优化方法。
5. 递归的分类
5.1 直接递归(Direct Recursion)
方法直接调用自身,这是最常见的形式。
void DirectRecursion()
{
// ... 一些逻辑 ...
DirectRecursion(); // 直接调用自己
}
5.2 间接递归(Indirect Recursion)
方法 A 调用方法 B,方法 B 又调用方法 A,形成循环调用。
using System;
class Program
{
static void MethodA(int n)
{
if (n <= 0) return;
Console.WriteLine($"A: {n}");
MethodB(n - 1); // A 调用 B
}
static void MethodB(int n)
{
if (n <= 0) return;
Console.WriteLine($"B: {n}");
MethodA(n - 1); // B 调用 A
}
static void Main()
{
MethodA(5);
// 输出:
// A: 5
// B: 4
// A: 3
// B: 2
// A: 1
}
}
5.3 尾递归(Tail Recursion)
递归调用是方法中的最后一步操作,递归调用的返回值直接被返回,不再参与其他计算。
// 普通递归(不是尾递归)
// 因为 n * Factorial(n - 1) 中,Factorial(n-1) 返回后还要做乘法
static long Factorial(int n)
{
if (n == 0) return 1;
return n * Factorial(n - 1); // 返回值还要参与乘法运算
}
// 尾递归版本
// 递归调用是方法的最后一步,返回值直接传递
static long FactorialTail(int n, long accumulator = 1)
{
if (n == 0) return accumulator;
return FactorialTail(n - 1, n * accumulator); // 最后一步,直接返回
}
尾递归的优势:编译器和运行时可以将其优化为迭代形式,避免栈溢出。但需要说明的是,C# 编译器目前不原生支持尾递归优化(TCO),在 C# 中使用尾递归主要是代码风格上的选择。
5.4 树形递归(Tree Recursion)
一次递归调用中多次调用自身,形成树状调用结构。斐波那契就是典型的树形递归。
Fib(5)
/ \
Fib(4) Fib(3)
/ \ / \
Fib(3) Fib(2) Fib(2) Fib(1)
/ \ / \ ...
Fib(2) Fib(1) ...
树形递归会重复计算许多相同的子问题,通常可以用**记忆化(Memoization)**来优化。
6. 递归 vs 迭代
很多递归问题都可以用循环(迭代)来实现,那什么时候用递归呢?
6.1 阶乘的迭代版本
// 递归版本
static long FactorialRecursive(int n)
{
if (n == 0) return 1;
return n * FactorialRecursive(n - 1);
}
// 迭代版本
static long FactorialIterative(int n)
{
long result = 1;
for (int i = 1; i <= n; i++)
{
result *= i;
}
return result;
}
6.2 对比分析
| 维度 | 递归 | 迭代 |
|---|---|---|
| 代码简洁性 | 代码更简洁,接近数学定义 | 代码可能更冗长 |
| 可读性 | 对树/图等结构天然易读 | 线性问题更易读 |
| 内存消耗 | 每次调用消耗栈空间 | 通常只用固定变量 |
| 性能 | 有函数调用开销 | 通常更快 |
| 栈溢出风险 | 深度过大可能溢出 | 无栈溢出风险 |
| 调试难度 | 调用链复杂,较难调试 | 调试相对简单 |
6.3 选择原则
- 优先使用递归:处理树结构、图遍历、分治算法、回溯算法等天然递归的问题。
- 优先使用迭代:简单的循环逻辑、对性能要求极高的场景、递归深度不可控的场景。
7. 经典递归问题
7.1 汉诺塔(Tower of Hanoi)
有三根柱子 A、B、C。A 柱上有 n 个大小不等的圆盘,大的在下,小的在上。
目标:将所有圆盘从 A 柱移动到 C 柱。
规则:每次只能移动一个圆盘;大盘不能放在小盘上面。
using System;
class Program
{
/// <summary>
/// 递归求解汉诺塔
/// </summary>
/// <param name="n">圆盘数量</param>
/// <param name="from">起始柱子</param>
/// <param name="temp">辅助柱子</param>
/// <param name="to">目标柱子</param>
static void Hanoi(int n, char from, char temp, char to)
{
if (n == 1)
{
// 终止条件:只有一个盘子,直接移动
Console.WriteLine($"将盘子 1 从 {from} 移动到 {to}");
return;
}
// 1. 将 n-1 个盘子从 from 移到 temp(借助 to)
Hanoi(n - 1, from, to, temp);
// 2. 将第 n 个(最大的)盘子从 from 移到 to
Console.WriteLine($"将盘子 {n} 从 {from} 移动到 {to}");
// 3. 将 n-1 个盘子从 temp 移到 to(借助 from)
Hanoi(n - 1, temp, from, to);
}
static void Main()
{
Hanoi(3, 'A', 'B', 'C');
// 输出:
// 将盘子 1 从 A 移动到 C
// 将盘子 2 从 A 移动到 B
// 将盘子 1 从 C 移动到 B
// 将盘子 3 从 A 移动到 C
// 将盘子 1 从 B 移动到 A
// 将盘子 2 从 B 移动到 C
// 将盘子 1 从 A 移动到 C
}
}
移动次数:n 个盘子需要 2^n - 1 次移动。n=3 时需要 7 次,n=4 时需要 15 次。
7.2 二分查找(递归实现)
using System;
class Program
{
/// <summary>
/// 递归二分查找
/// </summary>
/// <param name="arr">已排序的数组</param>
/// <param name="target">目标值</param>
/// <param name="left">左边界</param>
/// <param name="right">右边界</param>
/// <returns>目标值的索引,未找到返回 -1</returns>
static int BinarySearch(int[] arr, int target, int left, int right)
{
// 终止条件:未找到
if (left > right)
{
return -1;
}
int mid = left + (right - left) / 2;
// 终止条件:找到了
if (arr[mid] == target)
{
return mid;
}
// 递归条件:在左半部分查找
else if (arr[mid] > target)
{
return BinarySearch(arr, target, left, mid - 1);
}
// 递归条件:在右半部分查找
else
{
return BinarySearch(arr, target, mid + 1, right);
}
}
static void Main()
{
int[] sortedArray = { 1, 3, 5, 7, 9, 11, 13, 15, 17, 19 };
int target = 13;
int index = BinarySearch(sortedArray, target, 0, sortedArray.Length - 1);
Console.WriteLine($"目标 {target} 的索引为: {index}"); // 输出: 6
}
}
7.3 反转字符串
static string ReverseString(string str)
{
// 终止条件:空字符串或单字符
if (str == null || str.Length <= 1)
{
return str;
}
// 递归:将第一个字符放到最后,反转剩余部分
return ReverseString(str.Substring(1)) + str[0];
}
// 使用示例
Console.WriteLine(ReverseString("Hello")); // 输出: olleH
Console.WriteLine(ReverseString("递归")); // 输出: 归递
7.4 最大公约数(GCD)- 欧几里得算法
/// <summary>
/// 递归计算两个数的最大公约数
/// 欧几里得算法:GCD(a, b) = GCD(b, a % b),直到 b = 0
/// </summary>
static int GCD(int a, int b)
{
// 终止条件
if (b == 0)
{
return a;
}
// 递归条件
return GCD(b, a % b);
}
// 使用示例
Console.WriteLine(GCD(48, 18)); // 输出: 6
Console.WriteLine(GCD(100, 45)); // 输出: 5
Console.WriteLine(GCD(17, 13)); // 输出: 1(互质)
7.5 数组求和
/// <summary>
/// 递归求数组元素之和
/// </summary>
static int SumArray(int[] arr, int index)
{
// 终止条件:已经处理完所有元素
if (index >= arr.Length)
{
return 0;
}
// 递归:当前元素 + 剩余元素之和
return arr[index] + SumArray(arr, index + 1);
}
// 使用示例
int[] numbers = { 1, 2, 3, 4, 5 };
Console.WriteLine(SumArray(numbers, 0)); // 输出: 15
8. 递归在实际开发中的应用
8.1 遍历文件夹(目录树)
这是递归最经典的实际应用之一。文件夹天然是树形结构。
using System;
using System.IO;
class Program
{
/// <summary>
/// 递归遍历目录下所有文件
/// </summary>
/// <param name="path">目录路径</param>
/// <param name="indent">缩进层级(用于格式化输出)</param>
static void ListFiles(string path, string indent = "")
{
try
{
// 输出当前目录名
Console.WriteLine($"{indent}[目录] {Path.GetFileName(path)}");
// 输出当前目录下的所有文件
foreach (string file in Directory.GetFiles(path))
{
Console.WriteLine($"{indent} [文件] {Path.GetFileName(file)}");
}
// 递归遍历子目录
foreach (string subDir in Directory.GetDirectories(path))
{
ListFiles(subDir, indent + " ");
}
}
catch (UnauthorizedAccessException)
{
Console.WriteLine($"{indent} [拒绝访问] {path}");
}
}
static void Main()
{
ListFiles(@"D:\TestFolder");
}
}
8.2 解析 JSON / XML 嵌套结构
using System;
using System.Xml;
class Program
{
/// <summary>
/// 递归打印 XML 节点树
/// </summary>
static void PrintXmlNode(XmlNode node, int depth = 0)
{
if (node == null) return;
// 打印当前节点
string indent = new string(' ', depth * 2);
Console.WriteLine($"{indent}<{node.Name}>");
// 递归处理子节点
foreach (XmlNode child in node.ChildNodes)
{
PrintXmlNode(child, depth + 1);
}
}
}
8.3 菜单/评论的层级展示
using System;
using System.Collections.Generic;
class Comment
{
public int Id { get; set; }
public int? ParentId { get; set; }
public string Content { get; set; }
}
class Program
{
/// <summary>
/// 递归展示评论的嵌套回复结构
/// </summary>
static void DisplayComments(List<Comment> allComments, int? parentId, string indent = "")
{
foreach (var comment in allComments)
{
// 找到当前层级的评论
if (comment.ParentId == parentId)
{
Console.WriteLine($"{indent}└─ [{comment.Id}] {comment.Content}");
// 递归显示该评论的回复
DisplayComments(allComments, comment.Id, indent + " ");
}
}
}
static void Main()
{
var comments = new List<Comment>
{
new Comment { Id = 1, ParentId = null, Content = "这篇文章写得真好!" },
new Comment { Id = 2, ParentId = 1, Content = "同意!作者的观点很清晰" },
new Comment { Id = 3, ParentId = 2, Content = "确实,尤其是第二部分" },
new Comment { Id = 4, ParentId = null, Content = "我有个疑问..." },
new Comment { Id = 5, ParentId = 4, Content = "请说,我们可以讨论" },
};
DisplayComments(comments, null);
// 输出:
// └─ [1] 这篇文章写得真好!
// └─ [2] 同意!作者的观点很清晰
// └─ [3] 确实,尤其是第二部分
// └─ [4] 我有个疑问...
// └─ [5] 请说,我们可以讨论
}
}
8.4 树结构的遍历(前序、中序、后序)
using System;
class TreeNode
{
public int Value { get; set; }
public TreeNode Left { get; set; }
public TreeNode Right { get; set; }
public TreeNode(int value) { Value = value; }
}
class BinaryTree
{
/// <summary>
/// 前序遍历:根 → 左 → 右
/// </summary>
public static void PreOrder(TreeNode node)
{
if (node == null) return;
Console.Write(node.Value + " ");
PreOrder(node.Left);
PreOrder(node.Right);
}
/// <summary>
/// 中序遍历:左 → 根 → 右(对于二叉搜索树,结果是升序的)
/// </summary>
public static void InOrder(TreeNode node)
{
if (node == null) return;
InOrder(node.Left);
Console.Write(node.Value + " ");
InOrder(node.Right);
}
/// <summary>
/// 后序遍历:左 → 右 → 根
/// </summary>
public static void PostOrder(TreeNode node)
{
if (node == null) return;
PostOrder(node.Left);
PostOrder(node.Right);
Console.Write(node.Value + " ");
}
static void Main()
{
// 构建二叉树:
// 1
// / \
// 2 3
// / \
// 4 5
TreeNode root = new TreeNode(1);
root.Left = new TreeNode(2);
root.Right = new TreeNode(3);
root.Left.Left = new TreeNode(4);
root.Left.Right = new TreeNode(5);
Console.Write("前序遍历: ");
PreOrder(root); // 输出: 1 2 4 5 3
Console.WriteLine();
Console.Write("中序遍历: ");
InOrder(root); // 输出: 4 2 5 1 3
Console.WriteLine();
Console.Write("后序遍历: ");
PostOrder(root); // 输出: 4 5 2 3 1
Console.WriteLine();
}
}
8.5 全排列(Permutations)/ 回溯算法
using System;
using System.Collections.Generic;
class Program
{
/// <summary>
/// 递归生成字符串的全排列
/// </summary>
static void Permute(string str, string answer = "")
{
// 终止条件:没有剩余字符,输出一个排列结果
if (str.Length == 0)
{
Console.WriteLine(answer);
return;
}
// 依次将每个字符作为当前第一个字符
for (int i = 0; i < str.Length; i++)
{
char ch = str[i];
string remaining = str.Substring(0, i) + str.Substring(i + 1);
Permute(remaining, answer + ch);
}
}
static void Main()
{
Console.WriteLine("字符串 \"ABC\" 的全排列:");
Permute("ABC");
// 输出:
// ABC
// ACB
// BAC
// BCA
// CAB
// CBA
}
}
8.6 深度优先搜索(DFS)
using System;
using System.Collections.Generic;
class Graph
{
private Dictionary<int, List<int>> adjacencyList = new();
public void AddEdge(int from, int to)
{
if (!adjacencyList.ContainsKey(from))
adjacencyList[from] = new List<int>();
if (!adjacencyList.ContainsKey(to))
adjacencyList[to] = new List<int>();
adjacencyList[from].Add(to);
adjacencyList[to].Add(from); // 无向图
}
/// <summary>
/// 递归深度优先搜索
/// </summary>
public void DFS(int node, HashSet<int> visited)
{
// 终止条件:如果节点已访问过则跳过
// 本实现用 visited 集合来防止无限递归
Console.WriteLine($"访问节点: {node}");
visited.Add(node);
foreach (int neighbor in adjacencyList[node])
{
if (!visited.Contains(neighbor))
{
DFS(neighbor, visited); // 递归访问邻居
}
}
}
static void Main()
{
Graph graph = new Graph();
graph.AddEdge(0, 1);
graph.AddEdge(0, 2);
graph.AddEdge(1, 3);
graph.AddEdge(1, 4);
graph.AddEdge(2, 5);
Console.WriteLine("DFS 遍历:");
graph.DFS(0, new HashSet<int>());
// 输出:
// 访问节点: 0
// 访问节点: 1
// 访问节点: 3
// 访问节点: 4
// 访问节点: 2
// 访问节点: 5
}
}
9. 递归的常见陷阱与注意事项
9.1 忘记设置终止条件(无限递归)
// ❌ 错误示例:没有终止条件,会导致栈溢出
static void InfiniteRecursion()
{
InfiniteRecursion(); // 永远不会停止!
}
// StackOverflowException: 引发了 System.StackOverflowException
教训:写递归方法时,第一步就是写终止条件。在纸上或脑海中先确认:什么情况下不再继续递归?
9.2 终止条件永远达不到
// ❌ 错误示例:n 的值永远不变,永远不会到达终止条件
static int BadRecursion(int n)
{
if (n == 0) return 1;
return BadRecursion(n); // 参数没有变小!
}
// ✅ 正确示例
static int GoodRecursion(int n)
{
if (n == 0) return 1;
return GoodRecursion(n - 1); // 参数朝终止条件递减
}
教训:确保每次递归调用时,参数都在朝终止条件收敛。
9.3 栈溢出(Stack Overflow)
每次递归调用都会消耗栈内存。C# 默认栈大小约为 1MB(32 位)或 4MB(64 位)。递归深度过大会导致 StackOverflowException。
// 这个调用在 n 很大时会抛出 StackOverflowException
static long SumToN(long n)
{
if (n <= 0) return 0;
return n + SumToN(n - 1);
}
// 实测:当 n 约为 10000-50000 之间时可能栈溢出(取决于框架和平台)
9.4 重复计算导致的性能问题
经典例子是斐波那契的朴素递归实现。计算 Fib(50) 需要约 2^50 次函数调用,这是天文数字。
// 优化方案:使用记忆化(Memoization)- 缓存中间结果
using System;
using System.Collections.Generic;
class Program
{
static Dictionary<int, long> memo = new Dictionary<int, long>();
static long FibonacciMemo(int n)
{
if (n == 0) return 0;
if (n == 1) return 1;
// 如果已经计算过,直接返回缓存结果
if (memo.ContainsKey(n))
{
return memo[n];
}
// 计算并缓存
long result = FibonacciMemo(n - 1) + FibonacciMemo(n - 2);
memo[n] = result;
return result;
}
static void Main()
{
Console.WriteLine(FibonacciMemo(50)); // 瞬间算出:12586269025
}
}
9.5 递归方法中修改全局状态
// ⚠️ 谨慎:递归中修改共享状态容易出错
static int counter = 0;
static int DangerousRecursion(int n)
{
counter++; // 每次调用都会修改
if (n <= 1) return n;
return DangerousRecursion(n - 1) + DangerousRecursion(n - 2);
}
// 如果多处调用,counter 的值会相互干扰
教训:递归方法应尽量使用局部变量和参数传递状态,避免依赖或修改全局/静态变量。
10. 尾递归与优化
10.1 什么是尾递归
尾递归是指递归调用是方法的最后一个操作,没有任何额外的计算跟在递归调用之后。
// 普通递归(非尾递归)
static int Factorial(int n)
{
if (n <= 1) return 1;
return n * Factorial(n - 1);
// ^^^^ 函数返回后还要做乘法 —— 不是尾递归
}
// 尾递归形式
static int FactorialTail(int n, int accumulator = 1)
{
if (n <= 1) return accumulator;
return FactorialTail(n - 1, n * accumulator);
// ^^^^^^^^^^^^^^^ 递归调用就是最后一步 —— 是尾递归
}
10.2 C# 中的尾递归现状
重要:C# 编译器(Roslyn)默认不会对尾递归进行优化。.NET 的 JIT 编译器在某些版本中可能会对特定情况做少量优化,但不应依赖它。
因此,在 C# 中写递归时,如果担心栈深度:
- 手动将递归改写为迭代(最可靠的做法)
- 使用
trampoline技术(利用委托模拟尾递归优化)
10.3 手工将递归转为迭代:蹦床技术(Trampoline)
using System;
class Program
{
/// <summary>
/// 使用蹦床技术将尾递归转为迭代,避免栈溢出
/// </summary>
static long FactorialTrampoline(int n)
{
long accumulator = 1;
while (n > 1)
{
accumulator = n * accumulator;
n--;
}
return accumulator;
}
// 更通用的蹦床模式演示
static T Trampoline<T>(Func<T, T> func, T initialValue, Func<T, bool> condition, Func<T, T> update)
{
T current = initialValue;
while (!condition(current))
{
current = func(current);
current = update(current);
}
return current;
}
}
11. 递归的调试技巧
11.1 添加缩进日志
跟踪递归调用的进入和退出,辅助理解执行流程。
using System;
class Program
{
static int FactorialDebug(int n, string indent = "")
{
Console.WriteLine($"{indent}→ 进入 Factorial({n})");
if (n == 0)
{
Console.WriteLine($"{indent}← 返回 1 (终止条件)");
return 1;
}
int result = n * FactorialDebug(n - 1, indent + " ");
Console.WriteLine($"{indent}← 返回 {result} (Factorial({n}))");
return result;
}
static void Main()
{
FactorialDebug(3);
// 输出:
// → 进入 Factorial(3)
// → 进入 Factorial(2)
// → 进入 Factorial(1)
// → 进入 Factorial(0)
// ← 返回 1 (终止条件)
// ← 返回 1 (Factorial(1))
// ← 返回 2 (Factorial(2))
// ← 返回 6 (Factorial(3))
}
}
11.2 使用断点和调用堆栈窗口
在 Visual Studio 或 VS Code 中:
- 在递归方法的第一行设置断点。
- 启动调试(F5)。
- 查看**"调用堆栈"(Call Stack)**窗口——可以看到当前所有未返回的递归调用层。
- 使用**"局部变量"(Locals)**窗口查看每一层的变量值。
- 按 F11(逐语句)跟踪每次递归调用和返回。
11.3 递归深度守卫
在生产代码中,可以设置最大递归深度,防止意外的栈溢出。
static int MaxDepth { get; set; } = 1000;
static void SafeRecursion(int depth)
{
if (depth > MaxDepth)
{
throw new InvalidOperationException(
$"递归深度超过最大限制 {MaxDepth},可能存在无限递归");
}
// 正常递归逻辑...
if (depth <= 0) return;
SafeRecursion(depth - 1);
}
12. 总结
递归的核心要点
| 要点 | 说明 |
|---|---|
| 定义 | 方法自己调用自己 |
| 两个要素 | 终止条件(出口) + 递归表达式(缩小规模) |
| 执行过程 | 递推(压栈) → 到达终止条件 → 回溯(出栈) |
| 适用场景 | 树结构遍历、分治算法、回溯算法、嵌套结构处理 |
| 主要风险 | 栈溢出、无限递归、重复计算 |
编写递归的步骤(四步法)
- 先写终止条件:明确最简单的情况,不需要递归就能得出答案。
- 拆解问题:把大问题拆成"一小步 + 一个规模更小的同类子问题"。
- 假设子问题已解决:相信递归调用能返回正确的子问题结果。
- 组合结果:用当前步骤的结果和子问题的结果组合出最终答案。
何时用递归,何时用迭代
递归更适合:
├── 树/图的遍历和搜索
├── 分治算法(归并排序、快速排序)
├── 动态规划 / 记忆化搜索
├── 回溯算法(排列、组合、N皇后)
└── 嵌套结构的处理(JSON、XML、目录、评论)
迭代更适合:
├── 简单的循环累加/累乘
├── 对性能要求极高的场景
├── 递归深度不可控的情况
└── 线性处理逻辑
一句话总结
递归就是用相同的方法解决规模更小的同类问题,直到问题小到可以直接解决为止。
文档完