bigywater.tech
算法与数据结构 · LeetCode 118

LeetCode 118. 杨辉三角

给定行数 numRows,生成杨辉三角的前 numRows 行:每行两端是 1,中间每个数等于上一行相邻两个数之和。一个容易踩的坑是不特判两端,直接用统一公式 row[j] = triangle[i-1][j-1] + triangle[i-1][j] 处理一整行——右边界 j=i 时会真正数组越界崩溃;左边界 j=0 时 triangle[i-1][-1] 在 Python 里不会报错,而是"成功"取到上一行最后一个元素,得到一个看起来合理实则错误的值,而且这个误差会一行行往后传递。本文用真实反例演示这两种失败模式,给出正确写法、5 种语言实现,以及逐行填表的分步演示。

2 种失败
不特判两端会导致右边界真正越界崩溃、左边界被 Python 负索引悄悄取错值这两种截然不同的失败模式
4 步
分步演示:每行先把两端设成 1,中间位置再用上一行相邻两数之和递推,另附 5 种语言实现

题意与示例

本文只用自己的话概括题意,完整约束以官方页面为准。给定行数 numRows,生成杨辉三角的前 numRows 行:每行两端是 1,中间每个数等于上一行相邻两个数之和。一共 5 组用例,覆盖:官方示例、单行、0 行、两行、七行:

真实实测 · 本机执行输出
numRows=5  →  [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1]]
numRows=1  →  [[1]]
numRows=0  →  []
numRows=2  →  [[1], [1, 1]]
numRows=7  →  [[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]]

陷阱:不特判两端,右边界越界崩溃、左边界被 Python 负索引悄悄取错值

一个容易踩的坑:想用统一公式 row[j] = triangle[i-1][j-1] + triangle[i-1][j] 处理一行里的每个位置,包括最左和最右两端,而不是先把两端直接设成 1。这里有两个边界:右边界 j = i 时,triangle[i-1][j] 会真正越界(上一行只有 i 个元素,下标最大是 i-1);左边界 j = 0 时,triangle[i-1][j-1] 也就是 triangle[i-1][-1]——Python 的负数下标不会报错,而是"成功"取到上一行的最后一个元素,得到一个看起来合理、实际错误的值:

真实实测 · 本机执行输出
右边界不做特判:j=i 时直接越界,程序在第 2 行就崩溃(IndexError)

如果把右边界用 j < len(triangle[i-1]) 稳妥地 guard 住、只漏掉左边界的特判,程序不会崩溃,但每一行的第一个数都会被悄悄算错:

真实实测 · 本机执行输出
第0行: 正确=[1]  负索引陷阱=[1]\n第1行: 正确=[1, 1]  负索引陷阱=[2, 1]\n第2行: 正确=[1, 2, 1]  负索引陷阱=[3, 3, 1]\n第3行: 正确=[1, 3, 3, 1]  负索引陷阱=[4, 6, 4, 1]\n第4行: 正确=[1, 4, 6, 4, 1]  负索引陷阱=[5, 10, 10, 5, 1]\n第5行: 正确=[1, 5, 10, 10, 5, 1]  负索引陷阱=[6, 15, 20, 15, 6, 1]

从第 2 行开始,每一行的第一个数都不再是 1,而是错误地取到了上一行最后一个数(恰好也是 1)加上一个本该不存在的贡献,误差还会一行行往后传递。这类问题在 Python 里格外隐蔽:换成 Java、Go 这些没有"负数下标自动取尾部元素"这个特性的语言,同样的疏忽会直接触发数组越界异常,反而更容易在开发阶段被发现。

两端先设1,中间递推:五种语言的实现

每一行先整体初始化成全 1(长度是 i+1,天然处理好两端),再单独用一个 range(1, i) 的循环只去更新中间那些确实需要"上一行左上 + 右上"的位置——两端从始至终都不会被这个内层循环碰到,自然不存在越界或负索引的问题。时间 O(numRows²),空间 O(numRows²)。Python、Java、Go、JavaScript、C++ 都是真实编译运行的,用同一组 5 个测试用例。

Python · 解法代码def generate(num_rows): triangle = [] for i in range(num_rows): row = [1] * (i + 1) # 每一行先把两端设成 1 for j in range(1, i): # 中间的元素才需要用上一行算 row[j] = triangle[i - 1][j - 1] + triangle[i - 1][j] triangle.append(row) return triangle
真实实测 · Python · 前几组用例的输出
$ python3 solution.py
numRows=5  →  [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1]]
numRows=1  →  [[1]]
numRows=0  →  []
numRows=2  →  [[1], [1, 1]]
numRows=7  →  [[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]]
Java · 解法代码class Solution { public List<List<Integer>> generate(int numRows) { List<List<Integer>> triangle = new ArrayList<>(); for (int i = 0; i < numRows; i++) { List<Integer> row = new ArrayList<>(Collections.nCopies(i + 1, 1)); // 两端先设成 1 for (int j = 1; j < i; j++) { // 中间的元素才需要用上一行算 row.set(j, triangle.get(i - 1).get(j - 1) + triangle.get(i - 1).get(j)); } triangle.add(row); } return triangle; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java && java -cp build Main
numRows=5  →  [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1]]
numRows=1  →  [[1]]
numRows=0  →  []
numRows=2  →  [[1], [1, 1]]
numRows=7  →  [[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]]
Go · 解法代码func generate(numRows int) [][]int { triangle := make([][]int, 0, numRows) for i := 0; i < numRows; i++ { row := make([]int, i+1) for k := range row { row[k] = 1 // 两端先设成 1 } for j := 1; j < i; j++ { // 中间的元素才需要用上一行算 row[j] = triangle[i-1][j-1] + triangle[i-1][j] } triangle = append(triangle, row) } return triangle }
真实实测 · Go · 前几组用例的输出
$ go run solution.go
numRows=5  →  [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1]]
numRows=1  →  [[1]]
numRows=0  →  []
numRows=2  →  [[1], [1, 1]]
numRows=7  →  [[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]]
JavaScript · 解法代码function generate(numRows) { const triangle = []; for (let i = 0; i < numRows; i++) { const row = new Array(i + 1).fill(1); // 两端先设成 1 for (let j = 1; j < i; j++) { // 中间的元素才需要用上一行算 row[j] = triangle[i - 1][j - 1] + triangle[i - 1][j]; } triangle.push(row); } return triangle; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js
numRows=5  →  [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1]]
numRows=1  →  [[1]]
numRows=0  →  []
numRows=2  →  [[1], [1, 1]]
numRows=7  →  [[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]]
C++ · 解法代码vector<vector<int>> generate(int numRows) { vector<vector<int>> triangle; for (int i = 0; i < numRows; i++) { vector<int> row(i + 1, 1); // 两端先设成 1 for (int j = 1; j < i; j++) { // 中间的元素才需要用上一行算 row[j] = triangle[i-1][j-1] + triangle[i-1][j]; } triangle.push_back(row); } return triangle; }
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin
numRows=5  →  [[1], [1, 1], [1, 2, 1], [1, 3, 3, 1], [1, 4, 6, 4, 1]]
numRows=1  →  [[1]]
numRows=0  →  []
numRows=2  →  [[1], [1, 1]]
numRows=7  →  [[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]]
真实实测 · 本机执行输出
共 5 组用例(含边界),以 Python 结果为基准逐组比对:\n  Python      通过 5/5\n  Java        通过 5/5\n  Go          通过 5/5\n  JavaScript  通过 5/5\n  C++         通过 5/5

随机对拍

Python 版本的递推实现另外和一个完全不依赖上一行、直接用组合数公式 C(i, j) 计算每个位置的版本做了随机对拍:

真实实测 · 本机执行输出
随机对拍 500 组(numRows 取 0 到 30),递推版与组合数公式版的结果全部一致,0 处不一致。

另外四种语言只在上面 5 组固定用例上验证过,没有做大规模随机对拍。

分步演示:两端先设1,中间用上一行相邻两数之和

下面的演示逐行生成杨辉三角的前 4 行:每一行先把两端设成 1,中间的每个位置再用上一行相邻两个数相加。蓝色是本行正在计算的位置,绿色是已经确定的值。每一帧都是把上面的解法在这组输入上真实执行记录下来的。

杨辉三角:两端先设1,中间用上一行相邻两数之和
本行正在计算的位置已经确定的值

    参考与说明

    • 官方题目页:LeetCode 118. 杨辉三角。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。
    • 没有覆盖:直接用组合数公式 C(i, j) 逐个计算的写法作为主解法展示(本文用它做了随机对拍验证)。
    京ICP备2026067502号-1京公网安备11010502063487号