假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例 1:
输入: n = 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶
示例 2:
输入: n = 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
提示:
1 <= n <= 45
1. 生活案例:跳跃的小青蛙
想象你是一只小青蛙,面前有一个 级的台阶。你的腿部力量比较特别:
- 技能 A:你可以用力一跳,跳 1 级台阶。
- 技能 B:你也可以猛地一发力,跳 2 级台阶。
你的疑惑:如果你想跳到顶层(第 级),到底有多少种不同的跳法组合?
逆向思维:
当你站在第 级台阶上回想最后一秒:
- 你可能是从第 级跳了 1 步上来的。
- 你也可能是从第 级跳了 2 步上来的。
- 除此之外,没有别的可能了!
- 结论:跳到 级的方法数 = 跳到 级的方法数 + 跳到 级的方法数。
2. 代码解析与“生活化”注释
这段代码同样采用了空间优化的策略,只记录了前两步的结果,非常高效。
JavaScript
/**
* @param {number} n - 楼梯的总级数
* @return {number} - 不同的爬法总数
*/
var climbStairs = function (n) {
// 生活化解释:
// 如果只有 1 级台阶,只有 1 种跳法;
// 如果有 2 级台阶,有 2 种跳法(1+1 或 直接跳2级)。
if (n <= 2) return n;
// left 相当于 dp[i-2],最初代表第 1 级台阶的方法数
let left = 1;
// right 相当于 dp[i-1],最初代表第 2 级台阶的方法数
let right = 2;
// res 用来存当前这一级台阶的方法数
let res = 0;
// 从第 3 级台阶开始算,一直算到第 n 级
for (let i = 3; i <= n; i++) {
// 关键逻辑:当前台阶的方法数 = 前一级的方法数 + 前两级的方法数
res = right + left;
// 算出当前级后,我们要往上挪一级,更新记忆:
// 原来的“前一级”变成了现在的“前两级”
left = right;
// 原来的“当前级”变成了现在的“前一级”
right = res;
}
return res;
};
3. 为什么代码这样写?(数学之美)
- 斐波那契规律:你会发现方法数序列是:1, 2, 3, 5, 8, 13... 每一个数都是前两个数的和。
- 拒绝递归:代码开头注释掉的
climbStairs(n-1) + climbStairs(n-2)是递归写法。虽然直观,但它会产生大量的重复计算(比如算 时要算 ,算 时又要再算一遍 )。 - 迭代(循环)的优势:你的代码从底向上算,每一步只做一次加法,时间复杂度是 ,空间复杂度是 ,这是这道题的最优解。
总结
这道题是 “打家劫舍” 的简化版:
- 打家劫舍:是挑大的加(
Math.max)。 - 爬楼梯:是全都要,直接加(
+)。