在算法面试的江湖里, “爬楼梯”(Climbing Stairs) 就像是武侠小说里的《太极长拳》——招式简单,却是内功心法的基础。很多初学者能一眼看出它是斐波那契数列的变种,但面试官真正考察的,是你能否从“暴力拆解”进化到“优雅优化”的思维全过程。
今天,我们就把这道题揉碎了、讲透了,带你从底层逻辑出发,一步步攀登算法的高峰。
一、 问题建模:为什么是树形结构?
首先,我们要理解问题的本质。假设你面前有 阶台阶,你每次只能跨 1 步 或 2 步。
1. 逆向思维(自顶向下)
f(10)
f(9) f(8)
f(8) f(7) f(7) f(6)
我们站在最高处(第 阶)回望:
- 发现是一个树状结构,因此得出使用递归解决问题
- 如果你最后一步跨的是 1 步,说明你之前已经站在了第 阶;
- 如果你最后一步跨的是 2 步,说明你之前已经站在了第 阶。
所以,到达第 阶的总方法数 ,必然等于到达 阶的方法数与到达 阶的方法数之和。这就是大名鼎鼎的公式:
2. 树形分解
为了求出 ,你需要先知道 和 ;为了知道 ,你又要知道 和 。这种层层嵌套的关系,在逻辑上形成了一棵巨大的二叉树。
二、 方案一:暴力递归(最纯粹但也最脆弱)
程序员的第一反应往往是:既然公式都出来了,直接写个函数调自己不就行了?
JavaScript
function climbStairs(n) {
// 【退出条件】递归的灵魂。没有它,程序会掉入深渊。
// 第1阶只有1种爬法(走1步);第2阶有2种(1+1 或 直接跨2步)
if(n === 1) return 1;
if(n === 2) return 2;
// 【递归调用】自顶向下不断分裂。
// 看起来很优雅,但背后却隐藏着恐怖的计算量。
return climbStairs(n-1) + climbStairs(n-2);
}
为什么这个方案会被面试官“嫌弃”?
- 重复计算: 观察上面的树形图,你会发现 被算了好多次, 被算的次数更多。这种重叠子问题会导致计算量呈指数级增长。
- 调用栈溢出(Stack Overflow): 每一个递归函数都需要在内存中开辟一个栈帧。当 很大时,成千上万个函数还没跑完就堆在一起,内存直接爆表。
- 时间复杂度: 。这是一个让任何系统都会瞬间瘫痪的数字。
三、 方案二:记忆化搜索(空间换时间)
既然“重复计算”是罪魁祸首,那我们能不能把算过的结果存起来?这就是空间换时间的策略。
JavaScript
// 使用立即执行函数(IIFE)创建一个闭包,保护 memo 变量
const climbStairs2 = (function() {
// 【闭包容器】memo 就像一个记事本,用来存储已经算出来的答案
const memo = {};
return function (n) {
if(n === 1) return 1;
if(n === 2) return 2;
// 【关键点:查表】在向下递归之前,先翻翻记事本。
// 如果 memo[n] 已经有值了,说明之前算过,直接拿走,效率瞬间起飞!
if(memo[n]) return memo[n];
// 【关键点:存表】如果没算过,就算一次,然后赶紧记在 memo 里。
memo[n] = climbStairs2(n-1) + climbStairs2(n-2);
return memo[n];
};
})();
在这里使用了闭包加上 立即执行(IIFE) 保护了内部数据
memo是一个常用且方便的小技巧
进阶评价
这一版代码将时间复杂度降到了 。虽然引入了 的空间复杂度来维护 memo 对象,但对比起性能的提升,这笔买卖稳赚不赔!这种方法在算法中被称为带备忘录的自顶向下法。
四、 方案三:自底向上(从递归走向动态规划)
面试官可能会继续追问:“能不能不使用递归,直接从第 1 阶推导到第 阶?”
这就是自底向上的思想。递归是“由大化小”,而动态规划(Dynamic Programming) 则是“由小筑大”。
1. 逻辑重构
我们不再从天而降,而是从地基开始盖楼:
2. 极致的代码实现
为了把空间利用到极致,我们甚至不需要一个长长的数组,只需要三个变量来记录当前和前两次的状态。
JavaScript
function climbStairs3(n) {
// 处理基础边界情况
if(n === 1) return 1;
if(n === 2) return 2;
// 【初始状态】这就是我们的“地基”
// prevPrev 代表前前一个阶梯的方法数 f(i-2)
// prev 代表前一个阶梯的方法数 f(i-1)
let prevPrev = 1;
let prev = 2;
let current;
// 【自底向上迭代】从第3阶开始,一步步往上爬到第 n 阶
for(let i = 3; i <= n; i++) {
// 【核心方程】当前状态 = 前两个状态之和
current = prevPrev + prev;
// 【滚动更新】超级关键点!
// 这一步是动态规划的精髓:我们要把窗口向右滑动。
// 原来的 prev 变成了下一次计算中的 prevPrev
// 原来的 current 变成了下一次计算中的 prev
prevPrev = prev;
prev = current;
}
return current;
}
五、 技术复盘:深度解析动态规划
通过爬楼梯这道题,我们其实已经完整走过了动态规划的三大支柱:
1. 状态定义
在这道题中,状态就是 f(i),它代表的是“到达第 阶台阶的方法总数”。面试时,第一步一定要清晰地定义状态。
2. 状态转移方程
也就是 。这是 DP 的核心逻辑,决定了你是如何从子问题的解推导出原问题的解。
3. 边界条件与初始值
和 是所有推导的起点。没有地基,再漂亮的方程也无从谈起。
六、 给面试者的特别建议
一定要记住:代码只是载体,思维才是灵魂。
在面试中,如果你能按照这个流程讲解:
- 先给最差解:展示你对问题的直观理解(递归)。
- 分析痛点:指出重复计算和内存开销(复杂度分析)。
- 给出改进方案:引入记忆化搜索(备忘录)。
- 升华最优解:通过滚动变量实现 空间复杂度的动态规划(自底向上)。
面试官对你的评价将不仅仅是“会写代码”,而是“具备深厚的底层算法素养和优化思维”。
七、 互动与课后思考
爬楼梯问题其实是动态规划中最简单的一维 DP。
思考题:如果题目改成了“你每次可以跨 1步、2步 或 3步”,你的状态转移方程该如何修改?代码中的初始状态需要增加吗?
欢迎在评论区留下你的代码实现,我们一起交流进步!