【LeetCode Hot100 刷题日记 (82/100)】118. 杨辉三角 —— 动态规划与组合数学的完美融合 ✨

5 阅读5分钟

📌 题目链接:118. 杨辉三角 - 力扣(LeetCode)

🔍 难度:简单 | 🏷️ 标签:数组、动态规划、数学、组合数学

⏱️ 目标时间复杂度:O(n²)

💾 空间复杂度:O(1) (不计返回值)


杨辉三角,这道看似“入门级”的题目,实则蕴含着丰富的数学背景和算法思想。它不仅是动态规划的经典模板题,更是理解组合数递推关系二项式展开、甚至帕斯卡恒等式的绝佳入口。在面试中,它常被用作考察候选人对基础数据结构操作递推建模能力的试金石。

本文将带你从题目本质出发,深入剖析其核心算法,并提供可直接用于面试的手写代码模板,助你稳稳拿下这一分!

🔍 题目分析

给定一个非负整数 numRows,要求生成杨辉三角的前 numRows 行。

关键性质回顾

  • i 行(从 0 开始)有 i + 1 个元素。
  • 每一行的首尾元素恒为 1
  • 中间任意位置 (i, j) 的值 = 上一行左上角 (i-1, j-1) + 正上方 (i-1, j) 的值。
  • 数学上,第 i 行第 j 列的值等于组合数 C(i, j)

💡 面试提示:如果面试官问“杨辉三角和组合数有什么关系?”,你可以直接回答:“第 n 行第 k 项就是 C(n, k),且满足帕斯卡恒等式 C(n, k) = C(n-1, k) + C(n-1, k-1)。”


🧠 核心算法及代码讲解

本题最自然、最符合直觉的解法是 动态规划(Dynamic Programming)

✅ 为什么是动态规划?

  • 子问题重叠:当前行完全依赖于上一行。
  • 最优子结构:每一行的构造只依赖前一行,无需回溯或全局信息。
  • 无后效性:一旦某行计算完成,后续行不再修改它。

📌 状态定义

  • ret[i][j] 表示杨辉三角第 i 行第 j 列的值。

🔄 状态转移方程

ret[i][0] = ret[i][i] = 1;                    // 边界条件:每行首尾为1
ret[i][j] = ret[i-1][j-1] + ret[i-1][j];     // 中间元素 = 左上 + 正上

🧱 初始化

  • 直接按行构建,无需额外初始化整个二维数组。

📝 代码逐行注释(C++)

class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        // 创建一个包含 numRows 个 vector<int> 的二维数组
        vector<vector<int>> ret(numRows);
        
        for (int i = 0; i < numRows; ++i) {
            // 调整第 i 行的大小为 i+1(因为第0行有1个元素)
            ret[i].resize(i + 1);
            
            // 每行的第一个和最后一个元素都是 1
            ret[i][0] = ret[i][i] = 1;
            
            // 填充中间元素:从 j=1 到 j=i-1
            for (int j = 1; j < i; ++j) {
                // 当前值 = 上一行左上 + 上一行正上
                ret[i][j] = ret[i - 1][j] + ret[i - 1][j - 1];
            }
        }
        return ret;
    }
};

注意ret[i - 1][j] 是“正上方”,ret[i - 1][j - 1] 是“左上方”。这个索引关系务必清晰,面试手写时容易出错!


🧩 解题思路(分步拆解)

  1. 明确输出结构:需要返回一个 vector<vector<int>>,即二维列表。

  2. 处理边界情况

    • numRows = 0 → 返回空(但题目保证 numRows >= 1)。
    • numRows = 1 → 直接返回 ``。
  3. 逐行构建

    • 对每一行 i(从 0 开始):

      • 创建长度为 i+1 的新行。
      • 设置首尾为 1。
      • 遍历中间位置 j ∈ [1, i-1],用上一行的两个相邻值相加。
  4. 返回结果

💡 技巧:可以先用 resize(i+1) 分配空间,再赋值,避免频繁 push_back,效率更高。


📊 算法分析

项目分析
时间复杂度O(numRows²) —— 总共要填 1 + 2 + ... + numRows ≈ numRows²/2 个数
空间复杂度O(1)(额外空间)—— 仅使用返回数组,不计其空间
是否原地?否,但空间最优(必须返回完整三角)
可优化点?若只需第 n 行(如 LeetCode 119),可用滚动数组优化至 O(n) 空间

🎯 面试加分项:提到“若扩展到杨辉三角 II(只求第 n 行)”,可进一步优化空间。


💻 完整代码(含测试)

C++ 版本

#include <bits/stdc++.h>
using namespace std;
using ll = long long;

class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> ret(numRows);
        for (int i = 0; i < numRows; ++i) {
            ret[i].resize(i + 1);
            ret[i][0] = ret[i][i] = 1;
            for (int j = 1; j < i; ++j) {
                ret[i][j] = ret[i - 1][j] + ret[i - 1][j - 1];
            }
        }
        return ret;
    }
};

// 测试
signed main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    Solution sol;
    
    // 测试用例 1
    auto res1 = sol.generate(5);
    cout << "numRows = 5:\n";
    for (const auto& row : res1) {
        cout << "[";
        for (int i = 0; i < row.size(); ++i) {
            cout << row[i];
            if (i != row.size() - 1) cout << ",";
        }
        cout << "]\n";
    }
    // 输出: [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]

    cout << "\n";

    // 测试用例 2
    auto res2 = sol.generate(1);
    cout << "numRows = 1:\n";
    for (const auto& row : res2) {
        cout << "[";
        for (int i = 0; i < row.size(); ++i) {
            cout << row[i];
            if (i != row.size() - 1) cout << ",";
        }
        cout << "]\n";
    }
    // 输出: 

    return 0;
}

JavaScript 版本

var generate = function(numRows) {
    const ret = [];

    for (let i = 0; i < numRows; i++) {
        // 创建长度为 i+1 的数组,全部初始化为 1
        const row = new Array(i + 1).fill(1);
        // 填充中间元素(首尾已为1,无需处理)
        for (let j = 1; j < row.length - 1; j++) {
            row[j] = ret[i - 1][j - 1] + ret[i - 1][j];
        }
        ret.push(row);
    }
    return ret;
};

// 测试
console.log("numRows = 5:");
console.log(generate(5));
// [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]

console.log("\nnumRows = 1:");
console.log(generate(1));
// 

🌟 结语

🌟 本期完结,下期见!🔥

👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!

💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!