面试官最爱问的爬楼梯算法题:从超时到秒杀,我只做了这三步优化

736 阅读4分钟

前言

最近在刷算法题时遇到了一个经典问题——爬楼梯。这个问题看似简单,但背后却蕴含着重要的算法思想。本文将通过爬楼梯问题详细讲解递归和动态规划这两种算法思想,帮助大家理解它们的优缺点以及适用场景。

问题描述

假设你正在爬楼梯,需要 n 阶才能到达楼顶。每次你可以爬 1 或 2 个台阶,问有多少种不同的方法可以爬到楼顶?

例如:

  • 当 n=2 时,有两种方法:1 阶+1 阶 或者 直接 2 阶
  • 当 n=3 时,有三种方法:1+1+1 阶、1+2 阶、2+1 阶

递归解法

思路分析

递归是解决这个问题最直观的方法。我们可以从终点开始逆向思考:

  • 要到达第 n 阶楼梯,最后一步可能是从第 n-1 阶走 1 步上来的
  • 也可能是从第 n-2 阶走 2 步上来的
  • 因此,到达第 n 阶的方法数等于到达第 n-1 阶和第 n-2 阶的方法数之和

递归公式可以表示为: f(n) = f(n-1) + f(n-2)

退出条件是:

  • 当 n=1 时,只有一种方法
  • 当 n=2 时,有两种方法

代码实现

下面是递归解法的代码:

const climbStairs = function(n) {
    if (n <= 2) {
        return n; 
    }
    return climbStairs(n - 1) + climbStairs(n - 2);
}

存在的问题

递归解法虽然直观,但存在严重的性能问题。当 n 较大时,计算时间会显著增加,甚至会导致栈溢出。这是因为递归过程中存在大量的重复计算。

例如,计算 f(5) 时,f(3) 会被多次计算:

  • f(5) = f(4) + f(3)
  • f(4) = f(3) + f(2)
  • 这里 f(3) 被计算了两次

随着 n 的增大,重复计算的次数呈指数级增长,导致时间复杂度为 O(2^n)。

优化递归:记忆化搜索

思路

为了避免重复计算,我们可以使用一个数组来存储已经计算过的结果。当需要计算某个值时,先检查数组中是否已经存在,如果存在则直接使用,否则再进行计算。

代码实现

const f = []; // 存储中间结果
const climbStairs = function(n) {
    if (n <= 2) {
        return n; 
    }
    if(f[n] === undefined) {
        f[n] = climbStairs(n - 1) + climbStairs(n - 2);
    }
    return f[n];
}

优化效果

通过记忆化搜索,我们将时间复杂度从 O(2^n) 降低到了 O(n),因为每个值只需要计算一次。在实际测试中,计算 n=45 的时间从原来的数秒降低到了几乎瞬间完成。

动态规划解法

思路分析

动态规划是解决这个问题的另一种方法,它采用自底向上的方式来解决问题。与递归的自顶向下不同,动态规划从最小的子问题开始,逐步计算出更大的问题的解。

对于爬楼梯问题,我们可以定义一个数组 f,其中 f[i] 表示到达第 i 阶楼梯的方法数。根据前面的分析,我们可以得到状态转移方程: f[i] = f[i-1] + f[i-2]

初始条件为:

  • f[1] = 1
  • f[2] = 2

代码实现

var climbStairs = function(n) {
    const f = [];
    f[1] = 1;
    f[2] = 2;
    
    // 迭代计算每个值
    for (let i = 3; i <= n; i++) {
        f[i] = f[i - 1] + f[i - 2];
    }
    
    return f[n];
}

动态规划的优势

动态规划的时间复杂度同样是 O(n),但它避免了递归调用带来的额外开销,因此在性能上可能会略优于记忆化搜索的递归解法。此外,动态规划的代码通常更加简洁明了,可读性更强。

复杂度对比

方法时间复杂度空间复杂度实现难度
普通递归O(2^n)O(n)简单
记忆化递归O(n)O(n)中等
动态规划O(n)O(n)中等

总结

爬楼梯问题虽然简单,但却很好地展示了递归和动态规划这两种重要的算法思想。通过这个问题,我们可以总结出以下几点:

  1. 递归适合从问题的终点逆向思考,但容易出现重复计算
  2. 记忆化搜索可以有效解决递归中的重复计算问题
  3. 动态规划采用自底向上的方式,避免了递归的额外开销
  4. 对于可以分解为重叠子问题的题目,考虑使用记忆化搜索或动态规划

在实际开发中,我们应该根据问题的特点选择合适的解法。对于小规模问题,递归可能是最简单的选择;而对于大规模问题,动态规划通常是更优的解决方案。

希望通过本文的讲解,大家对递归和动态规划有了更深入的理解。在后续的算法学习中,遇到类似的问题能够快速找到合适的解法。