1、递推是什么
递推就是“用前面的结果,推算后面的结果”,一步一步往前推。
从下到上 从已知到未知解决问题
数学中的递推例子
- 等差数列:aₙ = aₙ₋₁ + d,比如a₁=2,d=3,a₂=5,a₃=8……
- 斐波那契数列:F(n) = F(n-1) + F(n-2),先算F(1)、F(2),再一步步推到F(3)、F(4)……
2、递推的关键
- 有起点(初始值):要知道最开始的一个或几个数。
- 一步步往后推:每一步都用前面的结果。
3、经典例题
斐波那契数列:有一个数列:0、1、1、2、3、5、8、13、21、34...
public static int fibonacciDP(int n) {
if (n <= 1) return n;
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
变式
已知 f(n)f(1)=1;
n=1f(2)=1;
n=2f(3)=1;
n=3f(n)=2f(n-1)+3f(n-2) +f(n-3) n>=4
public static int fWithArray(int n) {
if (n <= 0) {
throw new IllegalArgumentException("n必须大于0");
}
if (n == 1 || n == 2 || n == 3) {
return 1;
}
// 使用数组存储所有计算结果
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 1;
dp[3] = 1;
for (int i = 4; i <= n; i++) {
dp[i] = 2 * dp[i - 1] + 3 * dp[i - 2] + dp[i - 3];
}
return dp[n];
}
爬楼梯问题:有 n 阶楼梯,每次可以爬 1 或 2 个台阶,有多少种不同的方法爬到楼顶?
public static int climbStairs(int n) {
if (n <= 2) return n;
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
母牛的故事
有一头母牛,从第二年起,它每年年初生一头小母牛。每头小母牛从第四个年头开始,每年年初也生一头小母牛。请编程实现在第n年的时候,共有多少头母牛?
先找规律
重点是找到关系式:f[n]=f[n-1]+f[n-3]
public static int calculateSequence(int n) {
if (n <= 0) {
return 0;
}
int[] f = new int[n];
if (n >= 1) f[0] = 1;
if (n >= 2) f[1] = 2;
if (n >= 3) f[2] = 3;
for (int i = 3; i < n; i++) {
f[i] = f[i-1] + f[i-3];
}
return f[n-1];
}
杨辉三角
public static void printTriangle(int n) {
for (int i = 0; i < n; i++) {
int number = 1;
for (int j = 0; j < n - i - 1; j++) {
System.out.print(" ");
}
for (int j = 0; j <= i; j++) {
System.out.printf("%4d", number);
number = number * (i - j) / (j + 1);
}
System.out.println();
}
}
不同路径问题
机器人从 m×n 网格的左上角到右下角,只能向右或向下移动,有多少条不同路径?
机器人从(0 , 0) 位置出发,到(m - 1, n - 1)终点。
确定dp数组(dp table)以及下标的含义:dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。
确定递推公式:想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]。此时在回顾一下 dp[i - 1][j] 表示啥,是从(0, 0)的位置到(i - 1, j)有几条路径,dp[i][j - 1]同理。dp[i][j] = dp[i - 1][j] + dp[i][j - 1],因为dp[i][j]只有这两个方向过来。
dp数组的初始化:如何初始化呢,首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。
确定遍历顺序:这里要看一下递推公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1],dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。这样就可以保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值的。
public static int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
// 初始化第一行和第一列
for (int i = 0; i < m; i++) {
dp[i][0] = 1;
}
for (int j = 0; j < n; j++) {
dp[0][j] = 1;
}
// 递推计算
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
硬币找零问题
给定不同面额的硬币和总金额,计算可以凑成总金额的最少硬币数
public static int coinChange(int[] coins, int amount) {
// dp[i] 表示凑成金额 i 所需的最少硬币数
int[] dp = new int[amount + 1];
// 初始化,将 dp 数组填充为最大值
Arrays.fill(dp, amount + 1);
dp[0] = 0; // 金额为 0 时不需要任何硬币
// 遍历所有金额
for (int i = 1; i <= amount; i++) {
// 遍历所有硬币
for (int coin : coins) {
if (coin <= i) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
4、递推和递归的区别
| 特性 | 递推 (Iteration) | 递归 (Recursion) |
|---|---|---|
| 基本思想 | 从已知条件出发,逐步推导未知结果 | 将问题分解为更小的同类子问题 |
| 执行方式 | 循环结构(for/while) | 函数自我调用 |
| 方向 | 自底向上(Bottom-up) | 自顶向下(Top-down) |
| 内存使用 | 通常较低,只存储必要状态 | 较高,需要维护调用栈 |
| 性能 | 通常更高效,无函数调用开销 | 可能有重复计算,函数调用有开销 |
| 代码可读性 | 相对直观,但可能冗长 | 更符合数学定义,简洁优雅 |
| 适用场景 | 线性问题、状态转移明确的问题 | 树形结构、分治问题、回溯问题 |
| 栈溢出风险 | 低 | 高(深度过大时) |
| 实现难度 | 容易理解和调试 | 需要理解递归树和终止条件 |