📌 题目链接: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]是“左上方”。这个索引关系务必清晰,面试手写时容易出错!
🧩 解题思路(分步拆解)
-
明确输出结构:需要返回一个
vector<vector<int>>,即二维列表。 -
处理边界情况:
numRows = 0→ 返回空(但题目保证numRows >= 1)。numRows = 1→ 直接返回 ``。
-
逐行构建:
-
对每一行
i(从 0 开始):- 创建长度为
i+1的新行。 - 设置首尾为 1。
- 遍历中间位置
j ∈ [1, i-1],用上一行的两个相邻值相加。
- 创建长度为
-
-
返回结果。
💡 技巧:可以先用
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!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!