初级递归题目
一只青蛙可以一次跳 1 级台阶或者一次跳 2 级台阶, 例如:
- 跳上第 1 级台阶只有一种跳法:直接跳 1 级即可。
- 跳上第 2 级台阶有两种跳法:每次跳 1 级,跳两次;或者一次跳 2 级。
问要跳上第 n 级台阶有多少种跳法?
1. 直接的写法(这个递归实现的时间复杂度是 O(2^n),对于较大的 n 值(例如超过 30),会变得非常慢。)
function f(n) {
if (n === 1) return 1;
if (n === 2) return 2;
return f(n - 1) + f(n - 2);
}
可以考虑使用动态规划或记忆化递归来提高效率。
2. 记忆化递归 (降低时间复杂度,空间复杂度基本不变)
- 时间复杂度:从 O(2^n) 降低到 O(n)。
- 空间复杂度:保持在 O(n)。
function f(n, obj = {}) {
if (n === 1) return 1;
if (n === 2) return 2;
// 检查缓存中是否已存在结果
if (obj[n]) {
return obj[n];
}
// 计算结果并存储到缓存
obj[n] = f(n - 1, obj) + f(n - 2, obj);
return obj[n];
}
3. 动态规划方法
- 时间复杂度: O(n)
- 空间复杂度: O(n)
function fibonacciDP(n) {
if (n === 1) return 1;
if (n === 2) return 2;
// 创建一个数组来保存 Fibonacci 值
const dp = new Array(n + 1);
dp[1] = 1; // f(1)
dp[2] = 2; // f(2)
// 使用动态规划填充数组
for (let i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n]; // 返回 f(n)
}
4. 迭代方法
- 时间复杂度: O(n)
- 空间复杂度: O(1)
function fibonacciIterative(n) {
if (n === 1) return 1;
if (n === 2) return 2;
let prev1 = 1; // f(1)
let prev2 = 2; // f(2)
let current;
// 迭代计算 Fibonacci 值
for (let i = 3; i <= n; i++) {
current = prev1 + prev2; // 计算当前 Fibonacci 值
prev1 = prev2; // 更新前一个值
prev2 = current; // 更新当前值
}
return current; // 返回 f(n)
}
时间复杂度和空间复杂度的概念
时间复杂度是一个算法在运行时所需要的时间的量度。它通常用大O符号表示,描述了算法在输入规模增加时,运行时间的增长趋势。
空间复杂度是一个算法在运行时所需要的存储空间的量度。它同样用大O符号表示,描述了算法在输入规模增加时,所需存储空间的增长趋势。
递归算法中的时间复杂度和空间复杂度
在递归算法中,时间复杂度和空间复杂度的分析通常需要考虑递归调用的次数和每次调用所需的额外空间。
斐波那契数列的例子
在没有缓存的情况下,计算斐波那契数列的递归算法的时间复杂度是指数级的,即 O(2^n),因为每次计算 f(n) 都会递归调用 f(n-1) 和 f(n-2),导致大量重复计算。
使用缓存后,时间复杂度降低为线性,即 O(n),因为每个 f(n) 只会计算一次,之后的调用都直接从缓存中获取结果。
空间复杂度方面,使用缓存的递归算法需要额外的空间来存储中间结果。空间复杂度为 O(n),因为我们需要一个大小为 n 的缓存来存储每个 f(n) 的结果。
例子中的时间复杂度和空间复杂度
在你提到的代码中:
if (obj[n]) {
return obj[n];
}
- 时间复杂度: 由于每个
f(n)只计算一次,时间复杂度为O(n)。 - 空间复杂度: 由于需要存储
n个结果,空间复杂度为O(n)。
通过使用缓存,我们显著降低了时间复杂度,从指数级降到线性级别,同时增加了一些空间开销来存储中间结果。