118. 杨辉三角
题目描述

给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
示例 1:
输入 : numRows = 5
输出 : [[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
示例 2:
输入 : numRows = 1
输出 : [[1]]
提示:
1 <= numRows <= 30
问题深度分析
这是一道数学组合问题 ,核心在于理解杨辉三角的数学规律 和动态规划思想 。杨辉三角是组合数学中的经典结构,每个数字代表组合数 C(n, k),具有对称性和递推关系。
问题本质
生成杨辉三角的前 numRows 行,其中:
- 第0行 :
[1] - 第i行 :有
i+1个元素 - 第i行第j个元素 :等于第
i-1行第j-1个元素和第j个元素之和 - 边界 :每行首尾元素都是
1
核心思想
动态规划 + 数学规律:
- 递推关系 :
triangle[i][j] = triangle[i-1][j-1] + triangle[i-1][j] - 边界条件 :每行首尾为
1 - 对称性:杨辉三角关于中心对称
- 组合数性质 :
C(n, k) = C(n-1, k-1) + C(n-1, k)
关键难点
- 索引边界:注意数组索引从0开始,避免越界
- 空间优化:如何用O(k)空间生成第k行
- 数学公式 :直接计算组合数
C(n, k) = n! / (k! * (n-k)!) - 对称性利用:只计算一半,另一半对称复制
算法对比
| 方法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 方法一:动态规划(二维数组) | O(n²) | O(n²) | 直观易懂,存储所有行 |
| 方法二:动态规划(滚动数组) | O(n²) | O(n) | 空间优化,只存储上一行 |
| 方法三:组合数公式 | O(n²) | O(n²) | 数学方法,直接计算 |
| 方法四:对称性优化 | O(n²) | O(n²) | 利用对称性,减少计算 |
说明:
n:行数numRows
算法流程图
主算法流程
渲染错误: Mermaid 渲染失败: Parse error on line 16: ... E5 --> E6[triangle[i][j] = triangle[i- -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'SQS'
方法一:动态规划详细流程
渲染错误: Mermaid 渲染失败: Parse error on line 2: ...] --> B[初始化result = []] B --> C[i = -----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'SQS'
杨辉三角结构示意图
第0行: 1
第1行: 1 1
第2行: 1 2 1
第3行: 1 3 3 1
第4行: 1 4 6 4 1
1
1
1
1
2
1
复杂度分析
时间复杂度
所有方法均为 O(n²):
- 需要生成
n行 - 第
i行有i+1个元素 - 总元素数:
1 + 2 + ... + n = n(n+1)/2 = O(n²) - 每个元素需要 O(1) 时间计算
空间复杂度
-
方法一(二维DP):O(n²)
- 存储所有
n行 - 总空间:
1 + 2 + ... + n = n(n+1)/2 = O(n²)
- 存储所有
-
方法二(滚动数组):O(n)
- 只存储上一行和当前行
- 空间:
O(n)(最大行长度)
-
方法三(组合数公式):O(n²)
- 存储所有计算结果
- 空间:
O(n²)
-
方法四(对称性优化):O(n²)
- 存储所有行
- 但计算量减半(利用对称性)
关键优化技巧
技巧一:滚动数组优化
核心思想:只保存上一行,当前行基于上一行计算。
go
// 方法二:滚动数组优化
func generate2(numRows int) [][]int {
if numRows == 0 {
return [][]int{}
}
result := [][]int{{1}}
for i := 1; i < numRows; i++ {
prevRow := result[i-1]
currRow := make([]int, i+1)
currRow[0] = 1
currRow[i] = 1
for j := 1; j < i; j++ {
currRow[j] = prevRow[j-1] + prevRow[j]
}
result = append(result, currRow)
}
return result
}
优势:
- 空间复杂度从 O(n²) 降到 O(n)
- 逻辑清晰,易于理解
技巧二:组合数公式
核心思想 :直接计算组合数 C(n, k) = n! / (k! * (n-k)!)。
go
// 方法三:组合数公式
func generate3(numRows int) [][]int {
if numRows == 0 {
return [][]int{}
}
result := make([][]int, numRows)
for i := 0; i < numRows; i++ {
row := make([]int, i+1)
for j := 0; j <= i; j++ {
row[j] = combination(i, j)
}
result[i] = row
}
return result
}
func combination(n, k int) int {
if k > n-k {
k = n - k // 利用对称性
}
res := 1
for i := 0; i < k; i++ {
res = res * (n - i) / (i + 1)
}
return res
}
优势:
- 数学方法,直接计算
- 可以利用对称性优化
技巧三:对称性优化
核心思想:只计算前半部分,后半部分对称复制。
go
// 方法四:对称性优化
func generate4(numRows int) [][]int {
if numRows == 0 {
return [][]int{}
}
result := make([][]int, numRows)
result[0] = []int{1}
for i := 1; i < numRows; i++ {
row := make([]int, i+1)
row[0] = 1
row[i] = 1
mid := (i + 1) / 2
for j := 1; j < mid; j++ {
row[j] = result[i-1][j-1] + result[i-1][j]
}
// 利用对称性复制
for j := mid; j < i; j++ {
row[j] = row[i-j]
}
result[i] = row
}
return result
}
优势:
- 计算量减半
- 利用数学对称性
技巧四:动态规划(标准方法)
核心思想:使用二维数组存储所有行,逐行计算。
go
// 方法一:动态规划(二维数组)
func generate1(numRows int) [][]int {
if numRows == 0 {
return [][]int{}
}
result := make([][]int, numRows)
for i := 0; i < numRows; i++ {
row := make([]int, i+1)
row[0] = 1
row[i] = 1
for j := 1; j < i; j++ {
row[j] = result[i-1][j-1] + result[i-1][j]
}
result[i] = row
}
return result
}
优势:
- 思路直观
- 易于理解和实现
- 时间复杂度 O(n²)
边界情况处理
1. numRows = 0
go
if numRows == 0 {
return [][]int{}
}
2. numRows = 1
go
// 直接返回 [[1]]
result := [][]int{{1}}
3. 每行首尾元素
go
row[0] = 1 // 首元素
row[i] = 1 // 尾元素
4. 索引边界检查
go
// 确保不越界
for j := 1; j < i; j++ { // j从1开始,到i-1结束
row[j] = result[i-1][j-1] + result[i-1][j]
}
测试用例设计
基础测试
-
numRows = 0
- 输入:
0 - 输出:
[]
- 输入:
-
numRows = 1
- 输入:
1 - 输出:
[[1]]
- 输入:
-
numRows = 2
- 输入:
2 - 输出:
[[1],[1,1]]
- 输入:
标准测试
-
numRows = 5
- 输入:
5 - 输出:
[[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
- 输入:
-
numRows = 6
- 输入:
6 - 输出:
[[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1],[1,5,10,10,5,1]]
- 输入:
边界测试
-
numRows = 30(最大值)
- 测试最大输入
- 验证性能和正确性
-
对称性验证
- 验证每行是否关于中心对称
- 验证
triangle[i][j] == triangle[i][i-j]
常见错误与陷阱
错误一:索引越界
错误代码:
go
for j := 0; j <= i; j++ {
row[j] = result[i-1][j-1] + result[i-1][j] // j=0时,j-1=-1越界
}
正确做法:
go
row[0] = 1 // 先设置首元素
for j := 1; j < i; j++ { // j从1开始
row[j] = result[i-1][j-1] + result[i-1][j]
}
row[i] = 1 // 再设置尾元素
错误二:忘记设置边界元素
错误代码:
go
row := make([]int, i+1)
for j := 1; j < i; j++ {
row[j] = result[i-1][j-1] + result[i-1][j]
}
// 忘记设置row[0]和row[i]
正确做法:
go
row := make([]int, i+1)
row[0] = 1 // 必须设置
row[i] = 1 // 必须设置
for j := 1; j < i; j++ {
row[j] = result[i-1][j-1] + result[i-1][j]
}
错误三:组合数计算溢出
错误代码:
go
func combination(n, k int) int {
numerator := factorial(n)
denominator := factorial(k) * factorial(n-k)
return numerator / denominator // 可能溢出
}
正确做法:
go
func combination(n, k int) int {
if k > n-k {
k = n - k // 利用对称性
}
res := 1
for i := 0; i < k; i++ {
res = res * (n - i) / (i + 1) // 逐步计算,避免溢出
}
return res
}
实战技巧总结
1. 杨辉三角的数学性质
- 组合数 :
triangle[i][j] = C(i, j) - 对称性 :
triangle[i][j] = triangle[i][i-j] - 递推关系 :
C(n, k) = C(n-1, k-1) + C(n-1, k)
2. 空间优化思路
- 滚动数组:只保存上一行
- 对称性:只计算一半
- 组合数公式:直接计算,无需存储所有行
3. 索引处理技巧
- 从1开始遍历中间元素 :
for j := 1; j < i; j++ - 先设置边界:首尾元素先设为1
- 注意数组长度 :第i行有
i+1个元素
4. 组合数计算优化
- 利用对称性 :
C(n, k) = C(n, n-k) - 逐步计算:避免阶乘溢出
- 选择较小的k:减少计算量
进阶扩展
扩展一:只返回第k行(LeetCode 119)
- 只需要生成第k行
- 空间复杂度可以优化到O(k)
扩展二:大数支持
- 使用大整数类型
- 处理溢出情况
扩展三:查询特定位置
- 给定
(i, j),返回triangle[i][j] - 可以直接计算
C(i, j)
扩展四:打印杨辉三角
- 格式化输出
- 居中对齐显示
应用场景
1. 组合数学
- 场景 :计算组合数
C(n, k) - 优势:杨辉三角提供了组合数的直观表示
2. 概率论
- 场景:二项分布计算
- 优势:二项系数就是杨辉三角的元素
3. 算法优化
- 场景:动态规划中的组合数计算
- 优势:预计算杨辉三角,快速查询
4. 数学教育
- 场景:教学演示
- 优势:直观展示数学规律
相关题目
- 119. 杨辉三角 II:只返回第k行
- 120. 三角形最小路径和:动态规划变种
- 组合数问题:利用杨辉三角计算组合数
总结
本题是动态规划和数学规律的经典结合,通过四种不同的方法,我们展示了:
- 二维DP方法:直观易懂,存储所有行
- 滚动数组方法:空间优化,只存储上一行
- 组合数公式方法:数学方法,直接计算
- 对称性优化方法:利用对称性,减少计算
关键要点:
- 杨辉三角的递推关系:
triangle[i][j] = triangle[i-1][j-1] + triangle[i-1][j] - 边界条件:每行首尾为1
- 空间优化:使用滚动数组
- 数学性质:组合数、对称性
掌握本题有助于理解动态规划思想 和数学组合问题,为后续更复杂的DP问题打下基础。
完整题解代码
go
package main
import (
"fmt"
)
// =========================== 方法一:动态规划(二维数组) ===========================
// 时间复杂度:O(n²),空间复杂度:O(n²)
func generate1(numRows int) [][]int {
if numRows == 0 {
return [][]int{}
}
result := make([][]int, numRows)
for i := 0; i < numRows; i++ {
row := make([]int, i+1)
row[0] = 1
row[i] = 1
for j := 1; j < i; j++ {
row[j] = result[i-1][j-1] + result[i-1][j]
}
result[i] = row
}
return result
}
// =========================== 方法二:动态规划(滚动数组优化) ===========================
// 时间复杂度:O(n²),空间复杂度:O(n)
func generate2(numRows int) [][]int {
if numRows == 0 {
return [][]int{}
}
result := [][]int{{1}}
for i := 1; i < numRows; i++ {
prevRow := result[i-1]
currRow := make([]int, i+1)
currRow[0] = 1
currRow[i] = 1
for j := 1; j < i; j++ {
currRow[j] = prevRow[j-1] + prevRow[j]
}
result = append(result, currRow)
}
return result
}
// =========================== 方法三:组合数公式 ===========================
// 时间复杂度:O(n²),空间复杂度:O(n²)
func generate3(numRows int) [][]int {
if numRows == 0 {
return [][]int{}
}
result := make([][]int, numRows)
for i := 0; i < numRows; i++ {
row := make([]int, i+1)
for j := 0; j <= i; j++ {
row[j] = combination(i, j)
}
result[i] = row
}
return result
}
// 计算组合数 C(n, k)
func combination(n, k int) int {
if k > n-k {
k = n - k // 利用对称性
}
res := 1
for i := 0; i < k; i++ {
res = res * (n - i) / (i + 1)
}
return res
}
// =========================== 方法四:对称性优化 ===========================
// 时间复杂度:O(n²),空间复杂度:O(n²)
func generate4(numRows int) [][]int {
if numRows == 0 {
return [][]int{}
}
result := make([][]int, numRows)
result[0] = []int{1}
for i := 1; i < numRows; i++ {
row := make([]int, i+1)
row[0] = 1
row[i] = 1
// 只计算前半部分
mid := (i + 1) / 2
for j := 1; j < mid; j++ {
row[j] = result[i-1][j-1] + result[i-1][j]
}
// 利用对称性复制后半部分
for j := mid; j < i; j++ {
row[j] = row[i-j]
}
result[i] = row
}
return result
}
// =========================== 工具函数:比较二维切片 ===========================
func equal2D(a, b [][]int) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if len(a[i]) != len(b[i]) {
return false
}
for j := range a[i] {
if a[i][j] != b[i][j] {
return false
}
}
}
return true
}
// =========================== 测试 ===========================
func main() {
fmt.Println("=== LeetCode 118: 杨辉三角 ===\n")
testCases := []struct {
name string
numRows int
expected [][]int
}{
{
name: "numRows = 0",
numRows: 0,
expected: [][]int{},
},
{
name: "numRows = 1",
numRows: 1,
expected: [][]int{{1}},
},
{
name: "numRows = 2",
numRows: 2,
expected: [][]int{{1}, {1, 1}},
},
{
name: "numRows = 5",
numRows: 5,
expected: [][]int{{1}, {1, 1}, {1, 2, 1}, {1, 3, 3, 1}, {1, 4, 6, 4, 1}},
},
{
name: "numRows = 6",
numRows: 6,
expected: [][]int{{1}, {1, 1}, {1, 2, 1}, {1, 3, 3, 1}, {1, 4, 6, 4, 1}, {1, 5, 10, 10, 5, 1}},
},
{
name: "numRows = 7",
numRows: 7,
expected: [][]int{{1}, {1, 1}, {1, 2, 1}, {1, 3, 3, 1}, {1, 4, 6, 4, 1}, {1, 5, 10, 10, 5, 1}, {1, 6, 15, 20, 15, 6, 1}},
},
}
methods := []struct {
name string
fn func(int) [][]int
}{
{"方法一:动态规划(二维数组)", generate1},
{"方法二:动态规划(滚动数组优化)", generate2},
{"方法三:组合数公式", generate3},
{"方法四:对称性优化", generate4},
}
allPassed := true
for _, method := range methods {
fmt.Printf("--- %s ---\n", method.name)
passed := 0
for i, tc := range testCases {
result := method.fn(tc.numRows)
if equal2D(result, tc.expected) {
fmt.Printf(" Test %d: ✓ PASSED\n", i+1)
passed++
} else {
fmt.Printf(" Test %d: ✗ FAILED\n", i+1)
fmt.Printf(" 输入: numRows = %d\n", tc.numRows)
fmt.Printf(" 期望: %v\n", tc.expected)
fmt.Printf(" 得到: %v\n", result)
allPassed = false
}
}
fmt.Printf("通过率: %d/%d\n\n", passed, len(testCases))
}
if allPassed {
fmt.Println("🎉 所有测试通过!")
} else {
fmt.Println("❌ 部分测试失败")
}
}