目 录CONTENT

文章目录

CSharp(十九) 方法递归详解

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

关键理解:递归分为两个阶段:

  1. 递推(Forward Phase):不断调用自身,问题规模逐步缩小,向终止条件靠近。
  2. 回溯(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# 中写递归时,如果担心栈深度:

  1. 手动将递归改写为迭代(最可靠的做法)
  2. 使用 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 中:

  1. 在递归方法的第一行设置断点
  2. 启动调试(F5)。
  3. 查看**"调用堆栈"(Call Stack)**窗口——可以看到当前所有未返回的递归调用层。
  4. 使用**"局部变量"(Locals)**窗口查看每一层的变量值。
  5. 按 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. 总结

递归的核心要点

要点 说明
定义 方法自己调用自己
两个要素 终止条件(出口) + 递归表达式(缩小规模)
执行过程 递推(压栈) → 到达终止条件 → 回溯(出栈)
适用场景 树结构遍历、分治算法、回溯算法、嵌套结构处理
主要风险 栈溢出、无限递归、重复计算

编写递归的步骤(四步法)

  1. 先写终止条件:明确最简单的情况,不需要递归就能得出答案。
  2. 拆解问题:把大问题拆成"一小步 + 一个规模更小的同类子问题"。
  3. 假设子问题已解决:相信递归调用能返回正确的子问题结果。
  4. 组合结果:用当前步骤的结果和子问题的结果组合出最终答案。

何时用递归,何时用迭代

递归更适合:
├── 树/图的遍历和搜索
├── 分治算法(归并排序、快速排序)
├── 动态规划 / 记忆化搜索
├── 回溯算法(排列、组合、N皇后)
└── 嵌套结构的处理(JSON、XML、目录、评论)

迭代更适合:
├── 简单的循环累加/累乘
├── 对性能要求极高的场景
├── 递归深度不可控的情况
└── 线性处理逻辑

一句话总结

递归就是用相同的方法解决规模更小的同类问题,直到问题小到可以直接解决为止。


文档完

0
博主关闭了当前页面的评论