#c

递归算法基础

从概念到实践的递归思维训练

#c
@鞠大龙
魅客科创中心

CONTENTS

目录

1. 递归概述

1. 递归概述

  • 递归(Recursion):一种通过函数调用自身来解决问题的方法
  • 核心思想
    • 将复杂问题分解为相似的子问题
    • 子问题的解决方式与原问题相同
    • 通过解决最简单的情况(基本情况)来终止递归
  • 实现方式:函数直接或间接调用自身
  • 特点
    • 代码简洁优雅
    • 问题分解自然
    • 需要注意递归深度
    • 可能存在重复计算
返回目录 信息学 递归算法基础 魅客科创中心

1. 递归的应用场景

  • 数学计算问题
    • 阶乘计算
    • 斐波那契数列
    • 组合数计算
  • 数据结构操作
    • 树的遍历
    • 链表操作
    • 图的搜索
  • 分治问题
    • 归并排序
    • 快速排序
    • 二分查找
返回目录 信息学 递归算法基础 魅客科创中心

2. 递归的基本类型

2.1 线性递归

  • 定义:每个递归调用最多产生一个新的递归调用
  • 特点
    • 递归调用链呈线性结构
    • 空间复杂度通常为O(n)
    • 易于理解和实现
  • 典型例子
    • 阶乘计算
    • 数组求和
    • 链表遍历
    • 字符串反转
// 阶乘计算的线性递归
int factorial(int n) {
    // 基本情况
    if (n == 0 || n == 1) {
        return 1;
    }
    
    // 递归调用
    return n * factorial(n - 1);
}

// 数组求和的线性递归
int arraySum(vector<int>& arr, int n) {
    // 基本情况
    if (n <= 0) {
        return 0;
    }
    
    // 递归调用
    return arr[n-1] + arraySum(arr, n-1);
}
返回目录 信息学 递归算法基础 魅客科创中心

2.2 树形递归

  • 定义:每个递归调用可能产生多个新的递归调用
  • 特点
    • 递归调用呈树状结构
    • 可能存在大量重复计算
    • 通常需要优化(如记忆化)
  • 典型例子
    • 斐波那契数列
    • 汉诺塔问题
    • 二叉树遍历
    • 排列组合生成
// 斐波那契数列的树形递归
int fibonacci(int n) {
    // 基本情况
    if (n <= 1) return n;
    
    // 递归调用
    return fibonacci(n-1) + fibonacci(n-2);
}

// 二叉树遍历的树形递归
void traverseTree(TreeNode* root) {
    if (!root) return;
    
    // 前序遍历
    cout << root->val << " ";
    traverseTree(root->left);
    traverseTree(root->right);
}
返回目录 信息学 递归算法基础 魅客科创中心

2.3 尾递归

  • 定义:递归调用是函数的最后一个操作
  • 特点
    • 可以被编译器优化为迭代
    • 不会导致栈溢出
    • 空间复杂度可以是
  • 典型例子
    • 阶乘(尾递归版本)
    • 数组求和(尾递归版本)
    • 链表反转
    • 最大公约数计算
// 阶乘的尾递归版本
int factorialTail(int n, int acc = 1) {
    if (n <= 1) return acc;
    return factorialTail(n - 1, n * acc);
}

// 数组求和的尾递归版本
int arraySumTail(vector<int>& arr, 
                 int n, int sum = 0) {
    if (n <= 0) return sum;
    return arraySumTail(arr, n-1, 
                       sum + arr[n-1]);
}
返回目录 信息学 递归算法基础 魅客科创中心

2.4 互递归

  • 定义:两个或多个函数相互递归调用
  • 特点
    • 函数之间相互依赖
    • 问题可以自然地分为多个子问题
    • 需要注意函数声明顺序
  • 典型例子
    • 奇偶数判断
    • 表达式求值
    • 语法分析
    • 博弈问题求解
// 奇偶数判断的互递归
bool isEven(int n);  // 前向声明

bool isOdd(int n) {
    if (n == 0) return false;
    return isEven(n - 1);
}

bool isEven(int n) {
    if (n == 0) return true;
    return isOdd(n - 1);
}

// 表达式求值的互递归
int term();  // 前向声明

int factor() {
    // 解析因子
    if (当前是数字) return 数字值;
    if (当前是左括号) {
        消耗左括号;
        int val = term();
        消耗右括号;
        return val;
    }
}

int term() {
    // 解析项
    int val = factor();
    while (当前是乘除运算符) {
        char op = 当前运算符;
        消耗运算符;
        int right = factor();
        val = (op == '*') ? val * right : val / right;
    }
    return val;
}
返回目录 信息学 递归算法基础 魅客科创中心

3. 递归的设计步骤

3. 递归的设计步骤

  1. 识别基本情况:确定递归的终止条件
  2. 定义递归关系:找出问题与子问题的关系
  3. 确保收敛:保证递归能够达到基本情况
  4. 处理返回值:定义如何组合子问题的解
  5. 考虑效率:评估是否需要优化(如记忆化)
  6. 处理边界条件:考虑特殊输入和错误情况
返回目录 信息学 递归算法基础 魅客科创中心

4. 递归模板代码

4.1 递归模板代码:基本结构

基本递归模板

ReturnType recursiveFunction(Parameters params) {
    // 1. 基本情况检查
    if (baseCase) {
        return baseValue;
    }
    
    // 2. 处理当前层(可选)
    // 进行一些必要的处理
    
    // 3. 递归调用
    ReturnType result = recursiveFunction(smallerProblem);
    
    // 4. 处理返回值(可选)
    // 根据需要处理递归调用的结果
    
    return finalResult;
}

带记忆化的递归模板

ReturnType memoizedFunction(Parameters params) {
    // 1. 检查是否已计算
    if (已经计算过) {
        return 缓存的结果;
    }
    
    // 2. 基本情况检查
    if (baseCase) {
        return baseValue;
    }
    
    // 3. 递归调用
    ReturnType result = memoizedFunction(smallerProblem);
    
    // 4. 存储结果
    缓存结果;
    
    return result;
}
返回目录 信息学 递归算法基础 魅客科创中心

4.2 递归模板代码:尾递归优化

普通递归转尾递归的模板

ReturnType recursiveFunction(Parameters params) {
    return recursiveHelper(params, 初始累积值);
}
ReturnType recursiveHelper(Parameters params, AccumulatorType acc) {
    // 1. 基本情况检查
    if (baseCase) {
        return acc;
    }
    
    // 2. 计算新的累积值
    AccumulatorType newAcc = 更新累积值;
    
    // 3. 尾递归调用
    return recursiveHelper(smallerProblem, newAcc);
}

示例:阶乘计算

int factorial(int n) {
    return factorialHelper(n, 1);
}
int factorialHelper(int n, int acc) {
    if (n <= 1) return acc;
    return factorialHelper(n - 1, n * acc);
}

示例:数组求和

int arraySum(vector<int>& arr) {
    return arraySumHelper(arr, arr.size(), 0);
}
int arraySumHelper(vector<int>& arr, int n, int sum) {
    if (n <= 0) return sum;
    return arraySumHelper(arr, n - 1, sum + arr[n-1]);
}
返回目录 信息学 递归算法基础 魅客科创中心

5. 优化技巧

5. 优化技巧

  • 记忆化:存储已计算结果,避免重复计算
    • 使用数组或哈希表存储中间结果
    • 适用于有重叠子问题的递归
    • 可以显著提高效率
  • 尾递归优化:将普通递归转换为尾递归
    • 减少栈空间使用
    • 避免栈溢出
    • 可能被编译器优化为循环
  • 参数优化:减少参数传递和复制
    • 使用引用传递
    • 避免不必要的对象复制
    • 合理设计参数类型
  • 递归深度控制:避免栈溢出
    • 设置最大递归深度
    • 考虑使用迭代替代
    • 分析空间复杂度
返回目录 信息学 递归算法基础 魅客科创中心

6. 经典例题

6.1 汉诺塔问题:问题分析

问题描述

  • 有三根柱子A、B、C,A柱上有n个盘子
  • 盘子大小递减排列,大盘不能放在小盘上
  • 目标:将所有盘子从A移到C
  • 每次只能移动一个盘子

递归分析

  1. 基本思路

    • 将问题分解为移动较少盘子的子问题
    • 利用辅助柱子作为中转
    • 保持大盘在小盘下方的约束
  2. 递归过程

    graph TD
    A[移动n个盘子] --> B[移动n-1个盘子到辅助柱]
    B --> C[移动第n个盘子到目标柱]
    C --> D[移动n-1个盘子到目标柱]
    
  3. 复杂度分析

    • 时间复杂度:O(2^n)
    • 空间复杂度:O(n),递归栈深度
    • 移动次数:2^n - 1
  4. 关键步骤

    • 将n-1个盘子从源柱移到辅助柱
    • 将第n个盘子从源柱移到目标柱
    • 将n-1个盘子从辅助柱移到目标柱
返回目录 信息学 递归算法基础 魅客科创中心

6.1 汉诺塔问题:代码实现

/**
 * 汉诺塔递归求解函数
 * @param n: 要移动的盘子数量
 * @param from: 源柱子
 * @param aux: 辅助柱子
 * @param to: 目标柱子
 */
void hanoi(int n, char from, char aux, char to) {
    // 基本情况:只有一个盘子时直接移动
    if (n == 1) {
        cout << "Move disk 1 from " << from << " to " << to << endl;
        return;
    }
    
    // 递归步骤1:将n-1个盘子从源柱移到辅助柱
    hanoi(n - 1, from, to, aux);
    
    // 步骤2:将第n个盘子从源柱移到目标柱
    cout << "Move disk " << n << " from " << from << " to " << to << endl;
    
    // 递归步骤3:将n-1个盘子从辅助柱移到目标柱
    hanoi(n - 1, aux, from, to);
}

// 使用示例
int main() {
    int n = 3;  // 盘子数量
    hanoi(n, 'A', 'B', 'C');
    return 0;
}
返回目录 信息学 递归算法基础 魅客科创中心

6.2 归并排序:问题分析

问题描述

  • 输入:一个无序数组
  • 输出:按升序排列的有序数组
  • 要求:使用分治策略实现排序

分治策略分析

  1. 基本思路

    • 将数组分成两个子数组
    • 递归排序子数组
    • 合并已排序的子数组
  2. 递归过程

    graph TD
    A[原始数组] --> B[左半部分]
    A --> C[右半部分]
    B --> D[左左]
    B --> E[左右]
    C --> F[右左]
    C --> G[右右]
    D --> H[合并左半]
    E --> H
    F --> I[合并右半]
    G --> I
    H --> J[最终合并]
    I --> J
    
  3. 复杂度分析

    • 时间复杂度:O(n log n)
    • 空间复杂度:O(n)
    • 分解次数:log n
    • 每层合并:O(n)
  4. 优点与特性

    • 稳定排序算法
    • 适合大规模数据
    • 可并行化实现
返回目录 信息学 递归算法基础 魅客科创中心

6.2 归并排序:代码实现

/**
 * 合并两个已排序的子数组
 * @param arr: 原数组
 * @param left: 左边界
 * @param mid: 中间位置
 * @param right: 右边界
 */
void merge(vector<int>& arr, int left, int mid, int right) {
    vector<int> temp(right - left + 1);  // 临时数组
    int i = left, j = mid + 1, k = 0;
    
    // 合并两个有序数组
    while (i <= mid && j <= right) {
        if (arr[i] <= arr[j]) {
            temp[k++] = arr[i++];
        } else {
            temp[k++] = arr[j++];
        }
    }
    
    // 处理剩余元素
    while (i <= mid) temp[k++] = arr[i++];
    while (j <= right) temp[k++] = arr[j++];
    
    // 将临时数组复制回原数组
    for (i = 0; i < k; i++) {
        arr[left + i] = temp[i];
    }
}

/**
 * 归并排序主函数
 * @param arr: 待排序数组
 * @param left: 左边界
 * @param right: 右边界
 */
void mergeSort(vector<int>& arr, int left, int right) {
    // 基本情况:数组长度为1或0
    if (left >= right) return;
    
    // 计算中间位置
    int mid = left + (right - left) / 2;
    
    // 递归排序左半部分
    mergeSort(arr, left, mid);
    // 递归排序右半部分
    mergeSort(arr, mid + 1, right);
    
    // 合并已排序的子数组
    merge(arr, left, mid, right);
}

// 使用示例
int main() {
    vector<int> arr = {64, 34, 25, 12, 22, 11, 90};
    mergeSort(arr, 0, arr.size() - 1);
    
    cout << "Sorted array: ";
    for(int num : arr) cout << num << " ";
    return 0;
}
返回目录 信息学 递归算法基础 魅客科创中心

6.3 组合生成:问题分析

问题描述

  • 输入:两个整数n和k
  • 输出:所有可能的k个数的组合
  • 范围:数字从1到n
  • 要求:不能有重复组合,顺序无关

递归回溯分析

  1. 基本思路

    • 使用递归回溯法
    • 每个位置可以选择或不选择当前数字
    • 维护当前已选数字的集合
    • 当选择数量达到k时记录结果
  2. 递归树结构

    graph TD
    A[开始] --> B[选择1]
    A --> C[不选1]
    B --> D[选择2]
    B --> E[不选2]
    C --> F[选择2]
    C --> G[不选2]
    D --> H[...]
    E --> I[...]
    F --> J[...]
    G --> K[...]
    
  3. 优化策略

    • 剪枝:提前判断剩余数字是否足够
    • 空间优化:使用引用传递避免复制
    • 时间优化:合理设置起始位置
  4. 复杂度分析

    • 时间复杂度:O(C(n,k))
    • 空间复杂度:O(k),递归栈深度
返回目录 信息学 递归算法基础 魅客科创中心

6.3 组合生成:代码实现

/**
 * 生成组合的主函数
 * @param n: 数字范围上限
 * @param k: 要选择的数字个数
 * @return: 所有可能的组合
 */
vector<vector<int>> combine(int n, int k) {
    vector<vector<int>> result;  // 存储所有组合
    vector<int> current;         // 当前组合
    
    // 调用辅助函数进行递归生成
    combineDFS(n, k, 1, current, result);
    return result;
}

/**
 * 递归生成组合的辅助函数
 * @param n: 数字范围上限
 * @param k: 要选择的数字个数
 * @param start: 当前可选的起始数字
 * @param current: 当前已选数字集合
 * @param result: 存储所有组合的结果集
 */
void combineDFS(int n, int k, int start, 
                vector<int>& current, 
                vector<vector<int>>& result) {
    // 基本情况:找到一个合法组合
    if (current.size() == k) {
        result.push_back(current);
        return;
    }
    
    // 优化1:剪枝
    // 如果剩余数字不够凑成k个,直接返回
    if (current.size() + (n - start + 1) < k) return;
    
    // 递归生成组合
    for (int i = start; i <= n; i++) {
        // 选择当前数字
        current.push_back(i);
        
        // 递归生成剩余位置的组合
        combineDFS(n, k, i + 1, current, result);
        
        // 回溯:撤销选择
        current.pop_back();
    }
}

// 使用示例
int main() {
    int n = 4, k = 2;
    vector<vector<int>> combinations = combine(n, k);
    
    cout << "All combinations of " << k << " numbers from 1 to " << n << ":\n";
    for (const auto& comb : combinations) {
        cout << "{ ";
        for (int num : comb) cout << num << " ";
        cout << "}\n";
    }
    return 0;
}
返回目录 信息学 递归算法基础 魅客科创中心

6.4 表达式求值:问题分析

问题描述

  • 输入:包含数字和运算符的字符串表达式
  • 支持:加减乘除运算和括号
  • 输出:表达式的计算结果
  • 要求:正确处理运算符优先级

递归下降分析

  1. 基本原理

    • 将表达式分解为语法单元
    • 按照运算符优先级构建递归结构
    • 自顶向下分析表达式
  2. 语法树结构

    graph TD
    A[表达式] --> B[项]
    A --> C[+ -]
    A --> D[项]
    B --> E[因子]
    B --> F[* /]
    B --> G[因子]
    E --> H[数字/括号]
    G --> I[数字/括号]
    
  3. 优先级处理

    • 第一级:括号 ()
    • 第二级:乘除 * /
    • 第三级:加减 + -
  4. 实现策略

    • 递归处理不同优先级
    • 使用辅助函数分解任务
    • 错误处理和边界检查
返回目录 信息学 递归算法基础 魅客科创中心

6.4 表达式求值:代码实现

/**
 * 表达式计算器类
 * 使用递归下降法解析和计算数学表达式
 */
class ExpressionEvaluator {
private:
    string expr;    // 存储表达式
    int pos = 0;    // 当前处理位置
    
    /**
     * 跳过空白字符
     */
    void skipSpace() {
        while (pos < expr.length() && expr[pos] == ' ') pos++;
    }
    
    /**
     * 解析表达式(处理加减法)
     * @return 表达式的计算结果
     */
    int parseExpr() {
        int left = parseTerm();  // 先获取第一项
        
        while (pos < expr.length()) {
            skipSpace();
            if (pos >= expr.length()) break;
            
            char op = expr[pos];
            if (op != '+' && op != '-') break;
            
            pos++;  // 跳过运算符
            int right = parseTerm();
            
            // 执行加减运算
            if (op == '+') left += right;
            else left -= right;
        }
        
        return left;
    }
    
    /**
     * 解析项(处理乘除法)
     * @return 项的计算结果
     */
    int parseTerm() {
        int left = parseFactor();  // 先获取第一个因子
        
        while (pos < expr.length()) {
            skipSpace();
            if (pos >= expr.length()) break;
            
            char op = expr[pos];
            if (op != '*' && op != '/') break;
            
            pos++;  // 跳过运算符
            int right = parseFactor();
            
            // 执行乘除运算
            if (op == '*') left *= right;
            else {
                if (right == 0) throw runtime_error("Division by zero");
                left /= right;
            }
        }
        
        return left;
    }
    
    /**
     * 解析因子(处理数字和括号)
     * @return 因子的值
     */
    int parseFactor() {
        skipSpace();
        
        // 处理括号表达式
        if (expr[pos] == '(') {
            pos++;  // 跳过左括号
            int val = parseExpr();
            skipSpace();
            if (pos >= expr.length() || expr[pos] != ')') 
                throw runtime_error("Missing closing parenthesis");
            pos++;  // 跳过右括号
            return val;
        }
        
        // 处理数字
        if (!isdigit(expr[pos]))
            throw runtime_error("Invalid character: " + string(1, expr[pos]));
            
        int val = 0;
        while (pos < expr.length() && isdigit(expr[pos])) {
            val = val * 10 + (expr[pos] - '0');
            pos++;
        }
        
        return val;
    }
    
public:
    /**
     * 构造函数
     * @param s 要计算的表达式
     */
    ExpressionEvaluator(string s) : expr(s) {}
    
    /**
     * 计算表达式的值
     * @return 表达式的计算结果
     */
    int evaluate() {
        pos = 0;  // 重置位置
        int result = parseExpr();
        skipSpace();
        if (pos < expr.length())
            throw runtime_error("Invalid expression");
        return result;
    }
};

// 使用示例
int main() {
    string expr = "2 * (3 + 4) * 5";
    ExpressionEvaluator eval(expr);
    
    try {
        cout << "Expression: " << expr << endl;
        cout << "Result: " << eval.evaluate() << endl;
    } catch (const exception& e) {
        cout << "Error: " << e.what() << endl;
    }
    
    return 0;
}
返回目录 信息学 递归算法基础 魅客科创中心

7. 总结

7. 总结

  • 递归是一种重要的算法设计方法,通过将问题分解为相似的子问题来解决复杂问题
  • 掌握递归的核心思想:基本情况、递归关系、问题分解
  • 理解不同类型的递归:线性递归、树形递归、尾递归、互递归
  • 熟练运用优化技巧,提高递归效率
  • 递归与其他算法思想(如分治、动态规划)的结合,可以解决更复杂的问题
返回目录 信息学 递归算法基础 魅客科创中心

8. 练习题目

8.1 初级练习题

  1. [POJ 1664] 放苹果

    • 难度:★★☆☆☆
    • 描述:把M个相同的苹果放在N个相同的盘子里,允许有的盘子空着不放,求不同的放置方法数
    • 思路:使用递归,考虑最后一个盘子放或不放苹果的情况
  2. [HDU 2064] 汉诺塔III

    • 难度:★★☆☆☆
    • 描述:三根柱子的汉诺塔问题,求解最少移动次数
    • 思路:递归求解,分析每一步的移动规律
  3. [NOIP 2002] 分解因数

    • 难度:★★☆☆☆
    • 描述:将一个正整数n分解为若干个正整数的乘积,求不同的分解方法数
    • 思路:递归枚举每个因子,注意去重和顺序
返回目录 信息学 递归算法基础 魅客科创中心

8.2 中级练习题

  1. [POJ 2506] 超级楼梯

    • 难度:★★★☆☆
    • 描述:计算上n阶楼梯的不同方法数,每次可以上1或2阶
    • 思路:递归求解,使用记忆化优化
  2. [NOIP 2009] 分形之城

    • 难度:★★★☆☆
    • 描述:在分形图案中找到两点间的最短距离
    • 思路:递归分析分形结构,找出点的位置关系
  3. [CSP-J 2019] 次数查询

    • 难度:★★★☆☆
    • 描述:在递归生成的序列中查询某个数字出现的次数
    • 思路:分析递归规律,使用数学方法优化
返回目录 信息学 递归算法基础 魅客科创中心

8.3 高级练习题

  1. [NOIP 2014] 生成树计数

    • 难度:★★★★☆
    • 描述:计算特定图的生成树个数
    • 思路:使用递归和组合数学,考虑图的特殊结构
  2. [POJ 1192] 最优矩阵链乘

    • 难度:★★★★☆
    • 描述:给定矩阵链,求最优的乘法顺序
    • 思路:递归分治,使用记忆化搜索优化
  3. [USACO 2015] 递归函数

    • 难度:★★★★☆
    • 描述:分析递归函数的执行过程和结果
    • 思路:理解递归调用栈,找出规律
返回目录 信息学 递归算法基础 魅客科创中心

8.4 综合应用题

  1. [IOI 2000] 邮票

    • 难度:★★★★★
    • 描述:设计递归算法计算邮票组合方案
    • 思路:递归生成组合,考虑剪枝优化
  2. [NOIP 2016] 组合数问题

    • 难度:★★★★★
    • 描述:计算特定条件下的组合数
    • 思路:递归和数学归纳法结合
  3. [POJ 2115] C Looooops

    • 难度:★★★★★
    • 描述:分析递归程序的循环结构
    • 思路:理解递归和循环的关系,数学分析
返回目录 信息学 递归算法基础 魅客科创中心
递归算法基础

#c

演讲者备注不会在演示模式中显示给观众,仅供演讲者参考。 每个关键页面都添加了备注,帮助演讲者更好地讲解内容。

封面页: - 欢迎学生,简要介绍本次课程的主题和目标 - 强调递归是算法设计中的重要思想 - 本讲义将从基本概念到实际应用,系统讲解递归的思想和技巧

目录页: - 简要介绍讲义结构 - 递归基本概念、类型、设计步骤、模板代码、优化技巧、经典例题

转场页: - 引入递归算法的基本概念 - 递归是算法设计的基础思想之一

递归概述: - 递归的定义和核心思想 - 递归与迭代的区别

转场页: - 递归的常见类型

线性递归: - 定义和特点 - 常见的线性递归例子 - 线性递归的实现方式

树形递归: - 定义和特点 - 常见的树形递归例子 - 树形递归的实现方式

尾递归: - 定义和特点 - 常见的尾递归例子 - 尾递归的实现方式

互递归: - 定义和特点 - 常见的互递归例子 - 互递归的实现方式

转场页: - 递归算法的设计流程

转场页: - 递归算法的代码模板

转场页: - 递归算法的优化方法

转场页: - 递归经典例题

转场页: - 总结递归算法的核心思想和应用

转场页: - 递归算法练习题目 - 从各大OJ平台和竞赛中精选的递归题目

练习题目部分结束,这些题目涵盖了递归算法的各个难度和应用场景。 鼓励学生尝试解决这些问题,加深对递归思想的理解。