LeetCode 第118题:杨辉三角

152 阅读6分钟

LeetCode 第118题:杨辉三角

题目描述

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

在「杨辉三角」中,每个数是它左上方和右上方的数的和。

难度

简单

题目链接

点击在LeetCode中查看题目

示例

示例 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

解题思路

方法一:动态规划

杨辉三角的性质是每个数等于它左上方和右上方的数之和。我们可以利用这个性质,通过动态规划的方式逐行构建杨辉三角。

关键点:

  • 每行的第一个和最后一个元素都是1
  • 对于其他位置的元素,其值等于上一行的相邻两个元素之和

具体步骤:

  1. 创建一个二维列表result来存储杨辉三角
  2. 对于第一行,只有一个元素1
  3. 对于后续的每一行i(从1开始): a. 创建一个新的列表row,长度为i+1 b. 设置row[0]和row[i]为1 c. 对于row中的其他位置j(从1到i-1),设置row[j] = result[i-1][j-1] + result[i-1][j] d. 将row添加到result中
  4. 返回result

时间复杂度:O(numRows²),需要生成numRows行,每行最多有numRows个元素 空间复杂度:O(numRows²),需要存储杨辉三角的所有元素

方法二:数学公式

杨辉三角的每一行实际上是二项式展开的系数,可以使用组合数公式直接计算。

关键点:

  • 第n行第k个元素的值为组合数C(n,k),即C(n,k) = n! / (k! * (n-k)!)
  • 可以利用递推公式C(n,k) = C(n,k-1) * (n-k+1) / k来优化计算

具体步骤:

  1. 创建一个二维列表result来存储杨辉三角
  2. 对于每一行i(从0开始): a. 创建一个新的列表row,长度为i+1 b. 设置row[0]为1 c. 对于row中的其他位置j(从1到i),使用递推公式计算row[j] = row[j-1] * (i-j+1) / j d. 将row添加到result中
  3. 返回result

时间复杂度:O(numRows²),需要生成numRows行,每行最多有numRows个元素 空间复杂度:O(numRows²),需要存储杨辉三角的所有元素

图解思路

杨辉三角的构造过程

行号杨辉三角行构造方式
1[1]第一行只有一个元素1
2[1,1]第二行的首尾元素为1
3[1,2,1]第三行的首尾元素为1,中间元素2 = 1+1(上一行的相邻元素之和)
4[1,3,3,1]第四行的首尾元素为1,中间元素3 = 1+2,3 = 2+1(上一行的相邻元素之和)
5[1,4,6,4,1]第五行的首尾元素为1,中间元素4 = 1+3,6 = 3+3,4 = 3+1(上一行的相邻元素之和)

动态规划过程示意图

    1
   1 1
  1 2 1
 1 3 3 1
1 4 6 4 1

对于第5行的元素6,它等于第4行的相邻两个元素3和3的和:3 + 3 = 6。

代码实现

C# 实现

public class Solution {
    // 方法一:动态规划
    public IList<IList<int>> Generate(int numRows) {
        IList<IList<int>> result = new List<IList<int>>();
        
        // 处理第一行
        result.Add(new List<int> { 1 });
        
        // 处理后续行
        for (int i = 1; i < numRows; i++) {
            List<int> row = new List<int>();
            IList<int> prevRow = result[i - 1];
            
            // 每行的第一个元素总是1
            row.Add(1);
            
            // 计算中间元素
            for (int j = 1; j < i; j++) {
                row.Add(prevRow[j - 1] + prevRow[j]);
            }
            
            // 每行的最后一个元素总是1
            row.Add(1);
            
            result.Add(row);
        }
        
        return result;
    }
    
    // 方法二:数学公式
    public IList<IList<int>> GenerateWithMath(int numRows) {
        IList<IList<int>> result = new List<IList<int>>();
        
        for (int i = 0; i < numRows; i++) {
            List<int> row = new List<int>();
            
            // 每行的第一个元素总是1
            row.Add(1);
            
            // 使用组合数公式计算中间元素
            for (int j = 1; j <= i; j++) {
                // C(i,j) = C(i,j-1) * (i-j+1) / j
                row.Add(row[j - 1] * (i - j + 1) / j);
            }
            
            result.Add(row);
        }
        
        return result;
    }
}

Python 实现

class Solution:
    # 方法一:动态规划
    def generate(self, numRows: int) -> List[List[int]]:
        result = []
        
        # 处理第一行
        result.append([1])
        
        # 处理后续行
        for i in range(1, numRows):
            row = []
            prev_row = result[i - 1]
            
            # 每行的第一个元素总是1
            row.append(1)
            
            # 计算中间元素
            for j in range(1, i):
                row.append(prev_row[j - 1] + prev_row[j])
            
            # 每行的最后一个元素总是1
            row.append(1)
            
            result.append(row)
        
        return result
    
    # 方法二:数学公式
    def generateWithMath(self, numRows: int) -> List[List[int]]:
        result = []
        
        for i in range(numRows):
            row = []
            
            # 每行的第一个元素总是1
            row.append(1)
            
            # 使用组合数公式计算中间元素
            for j in range(1, i + 1):
                # C(i,j) = C(i,j-1) * (i-j+1) / j
                row.append(row[j - 1] * (i - j + 1) // j)
            
            result.append(row)
        
        return result

C++ 实现

class Solution {
public:
    // 方法一:动态规划
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> result;
        
        // 处理第一行
        result.push_back({1});
        
        // 处理后续行
        for (int i = 1; i < numRows; i++) {
            vector<int> row;
            const vector<int>& prevRow = result[i - 1];
            
            // 每行的第一个元素总是1
            row.push_back(1);
            
            // 计算中间元素
            for (int j = 1; j < i; j++) {
                row.push_back(prevRow[j - 1] + prevRow[j]);
            }
            
            // 每行的最后一个元素总是1
            row.push_back(1);
            
            result.push_back(row);
        }
        
        return result;
    }
    
    // 方法二:数学公式
    vector<vector<int>> generateWithMath(int numRows) {
        vector<vector<int>> result;
        
        for (int i = 0; i < numRows; i++) {
            vector<int> row;
            
            // 每行的第一个元素总是1
            row.push_back(1);
            
            // 使用组合数公式计算中间元素
            for (int j = 1; j <= i; j++) {
                // C(i,j) = C(i,j-1) * (i-j+1) / j
                row.push_back((long long)row[j - 1] * (i - j + 1) / j);
            }
            
            result.push_back(row);
        }
        
        return result;
    }
};

执行结果

C# 实现

  • 执行用时:176 ms
  • 内存消耗:36.2 MB

Python 实现

  • 执行用时:32 ms
  • 内存消耗:15.1 MB

C++ 实现

  • 执行用时:0 ms
  • 内存消耗:6.5 MB

性能对比

语言执行用时内存消耗特点
C#176 ms36.2 MB代码结构清晰,但性能较慢
Python32 ms15.1 MB代码简洁,性能适中
C++0 ms6.5 MB执行速度最快,内存占用最少

代码亮点

  1. 🎯 利用杨辉三角的性质,通过动态规划高效构建
  2. 💡 方法二使用数学公式直接计算,避免了重复计算
  3. 🔍 正确处理了边界情况,如第一行和每行的首尾元素
  4. 🎨 代码结构清晰,变量命名规范,易于理解和维护

常见错误分析

  1. 🚫 忽略了杨辉三角每行首尾元素都是1的特性
  2. 🚫 在计算中间元素时,索引计算错误
  3. 🚫 在使用数学公式时,没有考虑整数溢出问题
  4. 🚫 没有正确初始化第一行,导致后续计算错误

解法对比

解法时间复杂度空间复杂度优点缺点
动态规划O(numRows²)O(numRows²)实现简单,直观易懂需要存储所有中间结果
数学公式O(numRows²)O(numRows²)计算直接,无需依赖之前的行需要注意整数溢出问题

相关题目