LeetCode 70. 爬楼梯是入门级动态规划经典题,也是面试高频基础题。核心考点是理解“状态转移”逻辑,看似简单却能帮我们快速建立动态规划思维。本文将先回顾题目核心,再拆解一段爬楼梯TypeScript代码的逻辑、分析其优缺点,随后给出多套优化方案及补充方法,覆盖基础到进阶,适合新手快速吃透这道题。
一、题目回顾:快速把握核心需求
题目描述:假设你正在爬楼梯,需要 n 阶才能到达楼顶,每次可以爬 1 或 2 个台阶,求有多少种不同的爬楼方法?
核心规律(解题关键,后续所有方法均围绕此展开):
-
n=1:1种方法(直接爬1阶)
-
n=2:2种方法(1+1、直接爬2阶)
-
n≥3:第n阶的方法数 = 第n-1阶方法数 + 第n-2阶方法数(本质是斐波那契数列,仅初始值不同)
二、现有代码拆解:读懂逻辑,找准问题
先贴出待拆解代码,我们逐行分析其实现思路、核心逻辑,同时找出可优化的痛点:
function climbStairs(n: number): number {
let cur = 0;
let bef = -1;
for (let i = 0; i < n; i++) {
const next = (cur < 0 ? 0 : (cur === 0 ? 1 : cur)) + (bef < 0 ? 0 : (bef === 0 ? 1 : bef));
bef = cur;
cur = next;
}
return cur;
};
2.1 核心思路定位
这段代码的核心思路是「迭代递推」—— 用两个变量(cur、bef)记录“当前状态”和“前一个状态”的方法数,避免用数组存储所有状态,从而将空间复杂度优化至O(1),这是其核心优点。
这里的“状态”对应:cur 表示当前迭代轮次对应的台阶数的方法数,bef 表示上一轮(前一个台阶数)的方法数,通过循环逐步递推到第n阶,最终得到结果。
2.2 逐行拆解逻辑
-
变量初始化:let cur = 0; let bef = -1;
-
cur:初始值为0,用于记录“当前台阶数”的方法数;
-
bef:初始值为-1,用于记录“前一个台阶数”的方法数;
-
注:初始值看似特殊(bef=-1、cur=0),是为了通过后续判断,贴合爬楼梯的初始规律(n=1、n=2的情况)。
-
-
循环执行:for (let i = 0; i < n; i++) { ... }
-
核心计算:next 的取值逻辑(最关键部分),本质对应递推公式:下一个方法数 = 当前方法数 + 前一个方法数;
-
对 cur 的判断:cur < 0 取0,cur === 0 取1,否则取cur本身(处理初始值0和负数的特殊情况);
-
对 bef 的判断:和cur逻辑一致,bef < 0 取0,bef === 0 取1,否则取bef本身;
-
举例理解(n=1时):i=0(循环1次),next = (0===0?1:0) + (-1<0?0:-1) = 1+0=1,此时cur更新为1,最终返回1(符合n=1的正确结果)。
-
-
状态更新:bef = cur; cur = next;
-
每轮循环后,将当前的cur(当前方法数)赋值给bef(变为下一轮的“前一个方法数”);
-
将计算出的next(下一个方法数)赋值给cur(变为下一轮的“当前方法数”);
-
-
返回结果:循环n次后,cur即为第n阶的方法数。
2.3 代码优缺点分析(找准优化方向)
优点(值得肯定)
-
空间优化到位:仅使用2个变量(cur、bef),空间复杂度为O(1),避免了用数组存储所有台阶方法数的额外开销;
-
无栈溢出风险:采用迭代思路,而非递归,即使n较大(如n=45),也不会出现递归栈溢出的问题,时间复杂度为O(n),效率达标。
缺点(优化痛点)
-
逻辑冗余:对cur和bef的判断(cur<0、cur===0)完全可以简化,初始值的特殊情况可通过边界处理提前解决,无需每轮循环都判断;
-
可读性差:next的计算逻辑写在一行,包含多个三元运算符,不熟悉这段代码的人需要反复琢磨才能理解其意图;
-
初始值不直观:bef=-1、cur=0的初始值不符合爬楼梯的实际规律(台阶数的方法数不会是负数),增加了理解成本。
三、针对性优化方案:3种写法,逐步简化
优化核心:保留原代码“迭代递推+O(1)空间”的优点,简化冗余判断、优化初始值、提升可读性,同时保证代码正确性。以下3种优化写法,均保持O(n)时间复杂度、O(1)空间复杂度,适配不同理解习惯。
优化写法1:简化初始值(推荐,面试首选)
结合爬楼梯的初始规律(n=1→1种、n=2→2种),直接初始化前两个状态,去掉所有冗余判断,可读性拉满,也是面试中最稳妥的写法。
function climbStairs(n: number): number {
// 边界处理:提前解决n=1、n=2的情况,无需循环
if (n <= 2) return n;
// a = 第n-2阶方法数,b = 第n-1阶方法数(初始贴合n=1、n=2)
let a = 1, b = 2;
// 从n=3开始递推,直到n阶
for (let i = 3; i <= n; i++) {
const temp = a + b; // 当前n阶方法数 = 前两阶之和(递推核心)
a = b; // 更新n-2阶为原来的n-1阶
b = temp; // 更新n-1阶为当前n阶
}
return b; // 循环结束后,b即为第n阶方法数
};
优势:逻辑简洁、初始值直观,无任何冗余判断,新手也能一眼看懂,提交LeetCode可直接通过,适配题目所有测试用例。
优化写法2:兼容原代码变量逻辑,简化判断
如果想保留原代码的cur、bef变量命名习惯,仅简化next的计算逻辑,去掉不必要的三元判断,贴合原思路的同时提升可读性。
function climbStairs(n: number): number {
// 边界处理:提前解决n=1的特殊情况
if (n === 1) return 1;
// 初始值优化:贴合递推规律,去掉负数,无需额外判断
let cur = 1; // 对应n=1时的方法数
let bef = 1; // 辅助变量,适配递推逻辑
// 从n=2开始递推(n=1已提前处理)
for (let i = 2; i <= n; i++) {
const next = cur + bef; // 直接用递推公式,无需判断
bef = cur;
cur = next;
}
return cur;
};
优化写法3:极简版(适合追求代码简洁度)
在优化写法1的基础上,进一步简化代码,去掉临时变量temp,适合熟悉递推逻辑后使用。
function climbStairs(n: number): number {
let a = 1, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b]; // 解构赋值,简化状态更新
}
return b;
};
注:这里初始值a=1、b=1,是因为循环从i=2开始,递推到n时,b会自动对应第n阶的方法数,和题目规律完全匹配(n=2时,循环1次,b=1+1=2,正确)。
四、补充方法:覆盖基础到进阶,适配不同场景
除了上述迭代优化写法,以下补充2种常用方法,分别适配“理解递归思想”和“处理极大n”的场景,帮你全面掌握这道题的解题思路。
补充方法1:递归+记忆化(解决纯递归超时问题)
纯递归思路简单但会出现大量重复计算(时间复杂度O(2ⁿ)),当n≥30时就会超时。通过“记忆化”存储已计算过的结果,可将时间复杂度优化至O(n),空间复杂度O(n),适合理解递归思想和记忆化优化的核心。
function climbStairs(n: number): number {
// 用数组存储已计算的结果,避免重复计算(记忆化核心)
const memo = new Array(n + 1).fill(-1);
// 递归辅助函数
const dfs = (i: number): number => {
// 边界条件:i=0(地面)、i=1(1阶)均只有1种方法
if (i === 0 || i === 1) return 1;
// 若已计算过,直接返回结果,无需重复递归
if (memo[i] !== -1) return memo[i];
// 递推公式:第i阶方法数 = 第i-1阶 + 第i-2阶
memo[i] = dfs(i - 1) + dfs(i - 2);
return memo[i];
};
return dfs(n);
};
优势:思路直观,完美贴合“第n阶=第n-1阶+第n-2阶”的核心规律,适合新手理解递归与记忆化的结合;缺点:空间复杂度高于迭代法,需额外存储记忆数组。
补充方法2:矩阵快速幂(优化至O(logn)时间复杂度)
当n极大(如n=10⁶)时,O(n)的迭代法仍有优化空间,矩阵快速幂可将时间复杂度降至O(logn),是进阶优化方案,适合面试中体现代码功底(核心是利用矩阵乘法简化递推过程)。
function climbStairs(n: number): number {
// 定义矩阵乘法:两个2x2矩阵相乘
const multiply = (a: number[][], b: number[][]): number[][] => {
return [
[
a[0][0] * b[0][0] + a[0][1] * b[1][0],
a[0][0] * b[0][1] + a[0][1] * b[1][1]
],
[
a[1][0] * b[0][0] + a[1][1] * b[1][0],
a[1][0] * b[0][1] + a[1][1] * b[1][1]
]
];
};
// 定义矩阵快速幂:矩阵a的k次方(分治思想,降低时间复杂度)
const matrixPower = (a: number[][], k: number): number[][] => {
// 初始为单位矩阵(矩阵乘法的 identity 元素,类似乘法中的1)
let res = [[1, 0], [0, 1]];
while (k > 0) {
// 若k为奇数,先乘一次当前矩阵
if (k % 2 === 1) res = multiply(res, a);
// 矩阵自乘,k减半(分治核心)
a = multiply(a, a);
k = Math.floor(k / 2);
}
return res;
};
// 核心:爬楼梯递推可转化为矩阵幂运算:[[f(n+1),f(n)],[f(n),f(n-1)]] = [[1,1],[1,0]]^n
if (n <= 2) return n;
const matrix = [[1, 1], [1, 0]];
const powerMatrix = matrixPower(matrix, n - 1);
// f(n) = powerMatrix[0][0](结合矩阵递推规律)
return powerMatrix[0][0];
};
优势:时间复杂度O(logn),适合极大n的场景;缺点:逻辑相对复杂,需掌握矩阵乘法和快速幂的分治思想,日常开发和基础面试中使用较少,适合进阶学习。
五、总结:解题核心与方法选择
-
解题核心:爬楼梯问题的本质是斐波那契数列的应用,核心递推公式始终是「第n阶方法数 = 第n-1阶 + 第n-2阶」,所有方法均围绕此公式展开,区别仅在于实现方式和效率。
-
方法选择建议:
-
基础面试/日常练习:优先选择「优化写法1」,兼顾可读性和规范性,最容易被面试官认可;
-
追求代码简洁:选择「优化写法3」,用解构赋值简化状态更新,代码更简洁;
-
理解递归思想:选择「递归+记忆化」,直观贴合递推逻辑,适合新手入门;
-
进阶提升/处理极大n:选择「矩阵快速幂」,体现代码功底,应对极端场景。
- 优化技巧:无论哪种方法,优化的关键都是「减少冗余计算、优化空间开销」—— 迭代法通过变量替代数组优化空间,记忆化通过存储结果减少重复计算,矩阵快速幂通过分治思想降低时间复杂度。