C++递归与递推,从概念到实战

一、递归与递推:两种逆向思维的算法思想

在算法设计中,** 递归(Recursion)递推(Recurrence)** 是两种处理问题的核心思路,常用于解决具有重复子问题或递推关系的场景。它们的核心区别在于:

  • 递归:从问题本身出发,将复杂问题分解为规模更小的同类子问题,通过函数自身调用逐层解决,最终回溯得到结果(自顶向下)。
  • 递推:从已知的初始条件出发,通过递推公式逐步推导,最终得到目标结果(自底向上)。

二、递归:函数的自我调用艺术

(一)核心要素

  1. 递归终止条件:必须存在一个无需继续递归的基例(避免无限递归)。
  1. 递归表达式:将原问题转化为规模更小的子问题的表达式。

(二)经典案例:计算阶乘

问题:计算 \( n! = n \times (n-1) \times \dots \times 1 \)递归思路:\( n! = n \times (n-1)! \),基例为 \( 0! = 1 \)。

 
  

#include

using namespace std;

// 递归实现阶乘

int factorial(int n) {

if (n == 0) return 1; // 终止条件

return n * factorial(n-1); // 递归调用

}

int main() {

cout << "5! = " << factorial(5) << endl; // 输出120

return 0;

}

(三)递归的优缺点

优点

缺点

代码简洁,符合数学定义

多次重复计算,效率低下(如斐波那契)

适合树状 / 分治结构问题

可能导致栈溢出(递归深度过大)

(四)递归经典问题

  1. 斐波那契数列:\( f(n) = f(n-1) + f(n-2) \),基例 \( f(0)=0, f(1)=1 \)。
  1. 汉诺塔问题:通过递归移动 n 个盘子,每次将 n-1 个盘子视为整体。
  1. 二叉树遍历:前序、中序、后序遍历天然适合递归实现。

三、递推:从已知到未知的层层推导

(一)核心要素

  1. 初始条件:已知的最小规模问题的解(如斐波那契的 f (0) 和 f (1))。
  1. 递推公式:从当前解推导更大规模解的关系式。

(二)经典案例:斐波那契数列(递推实现)

问题:计算第 n 项斐波那契数 \( f(n) = f(n-1) + f(n-2) \)递推思路:从 f (0) 和 f (1) 开始,逐项计算到 f (n)(避免递归的重复计算)。

 
  

#include

using namespace std;

// 递推实现斐波那契

int fibonacci(int n) {

if (n == 0) return 0;

if (n == 1) return 1;

int a = 0, b = 1, c;

for (int i=2; i<=n; i++) {

c = a + b; // 递推公式

a = b; // 状态更新

b = c;

}

return b;

}

int main() {

cout << "f(10) = " << fibonacci(10) << endl; // 输出55

return 0;

}

(三)递推的分类

  1. 顺推法:从初始条件出发,正向推导(如斐波那契、阶乘的迭代计算)。
  1. 逆推法:从目标结果出发,反向推导初始条件(如 “猴子吃桃” 问题)。

(四)递推的典型应用

  1. 动态规划(DP):递推是 DP 的核心,通过状态转移方程(递推公式)和状态数组(存储中间解)优化计算。
    • 例:0-1 背包问题的递推式 \( dp[i][j] = \max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]) \)。
  1. 数列求和:如等差数列、等比数列的前 n 项和计算。
  1. 递推关系式问题:如卡特兰数、错排问题(全错位排列)。

四、递归 vs 递推:深度对比与选择

特征

递归

递推

思维方向

自顶向下(分解问题)

自底向上(逐步构建)

实现方式

函数调用自身

循环 + 迭代变量

空间复杂度

可能有较高栈空间消耗(递归深度)

通常较低(可优化为常数空间)

效率

可能重复计算(如朴素斐波那契)

无重复计算,效率更高

适用场景

树结构、分治问题、逻辑简洁场景

大规模数据、需要优化效率的场景

对比案例:计算斐波那契第 n 项

  • 递归(朴素版):时间复杂度 \( O(2^n) \),存在大量重复计算(如计算 f (5) 时 f (3) 被计算 2 次)。
  • 递推(迭代版):时间复杂度 \( O(n) \),空间复杂度 \( O(1) \)(仅需存储前两项)。

五、递归的优化:记忆化与递推化

(一)记忆化递归(Memoization)

在递归过程中存储子问题的解,避免重复计算,本质是递归 + 缓存(自顶向下的 DP)。

 
  

#include

#include

using namespace std;

vector memo(100, -1); // 存储中间结果

int fib_memo(int n) {

if (memo[n] != -1) return memo[n]; // 直接取缓存

if (n <= 1) return memo[n] = n;

return memo[n] = fib_memo(n-1) + fib_memo(n-2); // 计算并缓存

}

int main() {

cout << "f(50) = " << fib_memo(50) << endl; // 快速计算大数

return 0;

}

(二)递归转递推

将递归的调用栈手动转化为循环结构,避免栈溢出,同时保持高效。步骤

  1. 分析递归中的参数变化规律。
  1. 用循环变量模拟递归的参数传递。
  1. 按递推顺序计算并存储中间结果。

六、实战:用递归和递推解决汉诺塔问题

(一)问题描述

有 A、B、C 三根柱子,A 柱上有 n 个大小不同的盘子(下大上小),每次只能移动一个盘子,且任何时候大盘子不能放在小盘子上。求将所有盘子从 A 移到 C 的步骤。

(二)递归实现(思路清晰)

 
  

void hanoi(int n, char from, char to, char aux) {

if (n == 1) {

cout << "Move disk 1 from " << from << " to " << to << endl; // 基例

return;

}

hanoi(n-1, from, aux, to); // 将n-1个盘子从from移到aux(借助to)

cout << "Move disk " << n << " from " << from << " to " << to << endl; // 移动最大盘子

hanoi(n-1, aux, to, from); // 将n-1个盘子从aux移到to(借助from)

}

int main() {

hanoi(3, 'A', 'C', 'B'); // 输出7步移动过程

return 0;

}

(三)递推实现(复杂,需模拟栈)

汉诺塔的递推实现需用栈模拟递归过程,记录每个状态的参数(n, from, to, aux),适合理解递归的底层机制,但代码较复杂。

七、给新手的学习建议

  1. 先理解递归的终止条件:任何递归函数必须有明确的退出条件,否则会导致程序崩溃。
  1. 从小问题开始练习:从阶乘、斐波那契等简单问题入手,逐步尝试汉诺塔、二叉树遍历等复杂场景。
  1. 对比递归与递推的优劣:遇到问题时先思考 “是否有重复子问题”,若有则优先考虑递推或记忆化递归。
  1. 用调试工具观察过程:通过单步调试查看递归函数的调用栈,或递推过程中变量的变化规律。

总结:递归递推,殊途同归

递归和递推是解决具有层次结构或递推关系问题的两把利器:

  • 递归适合快速实现逻辑,尤其在树状结构(如文件系统遍历、语法解析)中优势明显;
  • 递推则在处理大规模数据或优化效率时更胜一筹,是动态规划等高级算法的基础。掌握这两种思想,能让你在面对 “斐波那契”“背包问题”“分治排序” 等问题时,根据场景选择最合适的解法。记住:递归是思维的简化,递推是效率的追求,二者结合(如记忆化递归)则能兼具简洁与高效!

都看会了吧,不会可以再评论区问,给个赞再走吧

你可能感兴趣的:(c++,算法,开发语言)