【LeetCode】118. 杨辉三角

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

核心思想

动态规划 + 数学规律

  1. 递推关系triangle[i][j] = triangle[i-1][j-1] + triangle[i-1][j]
  2. 边界条件 :每行首尾为 1
  3. 对称性:杨辉三角关于中心对称
  4. 组合数性质C(n, k) = C(n-1, k-1) + C(n-1, k)

关键难点

  1. 索引边界:注意数组索引从0开始,避免越界
  2. 空间优化:如何用O(k)空间生成第k行
  3. 数学公式 :直接计算组合数 C(n, k) = n! / (k! * (n-k)!)
  4. 对称性利用:只计算一半,另一半对称复制

算法对比

方法 时间复杂度 空间复杂度 特点
方法一:动态规划(二维数组) 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) 时间计算

空间复杂度

  1. 方法一(二维DP):O(n²)

    • 存储所有 n
    • 总空间:1 + 2 + ... + n = n(n+1)/2 = O(n²)
  2. 方法二(滚动数组):O(n)

    • 只存储上一行和当前行
    • 空间:O(n)(最大行长度)
  3. 方法三(组合数公式):O(n²)

    • 存储所有计算结果
    • 空间:O(n²)
  4. 方法四(对称性优化):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]
}

测试用例设计

基础测试

  1. numRows = 0

    • 输入:0
    • 输出:[]
  2. numRows = 1

    • 输入:1
    • 输出:[[1]]
  3. numRows = 2

    • 输入:2
    • 输出:[[1],[1,1]]

标准测试

  1. numRows = 5

    • 输入:5
    • 输出:[[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1]]
  2. numRows = 6

    • 输入:6
    • 输出:[[1],[1,1],[1,2,1],[1,3,3,1],[1,4,6,4,1],[1,5,10,10,5,1]]

边界测试

  1. numRows = 30(最大值)

    • 测试最大输入
    • 验证性能和正确性
  2. 对称性验证

    • 验证每行是否关于中心对称
    • 验证 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. 三角形最小路径和:动态规划变种
  • 组合数问题:利用杨辉三角计算组合数

总结

本题是动态规划和数学规律的经典结合,通过四种不同的方法,我们展示了:

  1. 二维DP方法:直观易懂,存储所有行
  2. 滚动数组方法:空间优化,只存储上一行
  3. 组合数公式方法:数学方法,直接计算
  4. 对称性优化方法:利用对称性,减少计算

关键要点

  • 杨辉三角的递推关系: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("❌ 部分测试失败")
	}
}
相关推荐
智算菩萨1 小时前
规模定律的边际递减与后训练时代的理论重构
人工智能·算法
kanhao1001 小时前
电平交叉采样 (Level-Crossing Sampling)
算法·fpga开发·fpga
Hcoco_me1 小时前
图像分割:目标检测、语义分割和实例分割
人工智能·深度学习·算法·目标检测·计算机视觉·目标跟踪
iAkuya1 小时前
(leetcode)力扣100 69有效的括号(栈)
算法·leetcode·职场和发展
运维闲章印时光1 小时前
企业跨地域互联:GRE隧道部署与互通配置
linux·服务器·网络
至此流年莫相忘1 小时前
Linux部署k8s(Ubuntu)
linux·ubuntu·kubernetes
We་ct1 小时前
LeetCode 21. 合并两个有序链表:两种经典解法详解
前端·算法·leetcode·链表·typescript
Epiphany.5562 小时前
蓝桥杯2024年第十五届决赛真题-套手镯
c++·算法·蓝桥杯
Σίσυφος19002 小时前
E = Kᵀ F K 的数学原理
算法