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

LeetCode 119. 杨辉三角 II

给定行下标 rowIndex,只返回杨辉三角的这一行,要求只用 O(rowIndex) 的额外空间。标准做法是反复原地更新同一个数组来模拟"从上一行推出下一行",但这里有个容易踩的坑:原地更新的方向如果从左往右,算到 row[j] 时用到的 row[j-1] 其实已经被本轮更新过、不再是上一行该有的旧值。本文用真实反例证明这个错误从第三个数就开始出错,给出正确的从右往左更新写法、5 种语言实现,以及原地更新过程的分步演示。

13 vs 10
rowIndex=5 时,从左往右原地更新在第三个数就把正确的 10 算成了 13
4 步
分步演示:数组全 1 初始化后,每一轮都从右往左原地更新,另附 5 种语言实现

题意与示例

本文只用自己的话概括题意,完整约束以官方页面为准。给定行下标 rowIndex(从 0 开始),只返回杨辉三角的这一行,进阶要求是只用 O(rowIndex) 的额外空间(不去存整个三角形)。一共 5 组用例,覆盖:0 行、1 行、3 行、官方示例(5 行)、7 行:

真实实测 · 本机执行输出
rowIndex=0  →  [1]
rowIndex=1  →  [1, 1]
rowIndex=3  →  [1, 3, 3, 1]
rowIndex=5  →  [1, 5, 10, 10, 5, 1]
rowIndex=7  →  [1, 7, 21, 35, 35, 21, 7, 1]

陷阱:原地更新的方向搞反,从左往右会用到本轮已经覆盖过的值

要做到 O(k) 空间,标准做法是只维护一个长度为 rowIndex+1 的数组,反复原地更新它、模拟"从上一行推出下一行"的过程。这里有个容易踩的坑:原地更新的方向搞反了。如果按照直觉从左往右更新 row[j] += row[j-1],那么当算到 row[j] 需要用 row[j-1] 时,row[j-1] 在本轮循环里其实已经被更新过一次了(提前混入了"新一行"的值),不再是"上一行"该有的旧值:

真实实测 · 本机执行输出
rowIndex=5(正确答案是 [1, 5, 10, 10, 5, 1])\n正确写法(从右往左原地更新)  →  [1, 5, 10, 10, 5, 1]\n陷阱写法(从左往右原地更新)  →  [1, 5, 13, 22, 23, 1]

从第 3 个数开始就已经完全错了:正确答案是 10,陷阱写法算出 13——因为算 row[2] 时用到的 row[1] 已经是本轮更新过的新值(5+旧row[0]的某种混合),不是上一行该有的旧值。这类"原地更新顺序"的陷阱在很多滚动数组优化里都会出现:只要新值依赖的是"旧的、还没被本轮更新触碰过"的数据,更新方向就必须避开这种覆盖,从右往左正好能保证每次用到的 row[j-1] 还是上一轮遗留下来的旧值。

O(k) 空间、原地从右往左更新:五种语言的实现

只用一个长度为 rowIndex+1、初始全是 1 的数组,模拟 rowIndex 次"从上一行推出下一行":每一轮从右往左更新 row[j] += row[j-1](j 从 i-1 递减到 1),这样在更新 row[j] 时,右边比它更靠右的位置已经是"新一行"的值,但 row[j-1] 还没被这一轮碰过,仍然是上一行遗留的旧值,天然正确。时间 O(rowIndex²),空间 O(rowIndex)。Python、Java、Go、JavaScript、C++ 都是真实编译运行的,用同一组 5 个测试用例。

Python · 解法代码def get_row(row_index): row = [1] * (row_index + 1) for i in range(1, row_index + 1): for j in range(i - 1, 0, -1): # 从右往左更新,避免用到"已经被这一轮覆盖过"的值 row[j] += row[j - 1] return row
真实实测 · Python · 前几组用例的输出
$ python3 solution.py
rowIndex=0  →  [1]
rowIndex=1  →  [1, 1]
rowIndex=3  →  [1, 3, 3, 1]
rowIndex=5  →  [1, 5, 10, 10, 5, 1]
rowIndex=7  →  [1, 7, 21, 35, 35, 21, 7, 1]
Java · 解法代码class Solution { public List<Integer> getRow(int rowIndex) { List<Integer> row = new ArrayList<>(Collections.nCopies(rowIndex + 1, 1)); for (int i = 1; i <= rowIndex; i++) { for (int j = i - 1; j > 0; j--) { // 从右往左更新,避免用到已经被这一轮覆盖过的值 row.set(j, row.get(j) + row.get(j - 1)); } } return row; } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java && java -cp build Main
rowIndex=0  →  [1]
rowIndex=1  →  [1, 1]
rowIndex=3  →  [1, 3, 3, 1]
rowIndex=5  →  [1, 5, 10, 10, 5, 1]
rowIndex=7  →  [1, 7, 21, 35, 35, 21, 7, 1]
Go · 解法代码func getRow(rowIndex int) []int { row := make([]int, rowIndex+1) for k := range row { row[k] = 1 } for i := 1; i <= rowIndex; i++ { for j := i - 1; j > 0; j-- { // 从右往左更新,避免用到已经被这一轮覆盖过的值 row[j] += row[j-1] } } return row }
真实实测 · Go · 前几组用例的输出
$ go run solution.go
rowIndex=0  →  [1]
rowIndex=1  →  [1, 1]
rowIndex=3  →  [1, 3, 3, 1]
rowIndex=5  →  [1, 5, 10, 10, 5, 1]
rowIndex=7  →  [1, 7, 21, 35, 35, 21, 7, 1]
JavaScript · 解法代码function getRow(rowIndex) { const row = new Array(rowIndex + 1).fill(1); for (let i = 1; i <= rowIndex; i++) { for (let j = i - 1; j > 0; j--) { // 从右往左更新,避免用到已经被这一轮覆盖过的值 row[j] += row[j - 1]; } } return row; }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js
rowIndex=0  →  [1]
rowIndex=1  →  [1, 1]
rowIndex=3  →  [1, 3, 3, 1]
rowIndex=5  →  [1, 5, 10, 10, 5, 1]
rowIndex=7  →  [1, 7, 21, 35, 35, 21, 7, 1]
C++ · 解法代码vector<int> getRow(int rowIndex) { vector<int> row(rowIndex + 1, 1); for (int i = 1; i <= rowIndex; i++) { for (int j = i - 1; j > 0; j--) { // 从右往左更新,避免用到已经被这一轮覆盖过的值 row[j] += row[j-1]; } } return row; }
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin
rowIndex=0  →  [1]
rowIndex=1  →  [1, 1]
rowIndex=3  →  [1, 3, 3, 1]
rowIndex=5  →  [1, 5, 10, 10, 5, 1]
rowIndex=7  →  [1, 7, 21, 35, 35, 21, 7, 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(rowIndex, j) 计算每个位置的版本做了随机对拍:

真实实测 · 本机执行输出
随机对拍 500 组(rowIndex 取 0 到 33),原地右到左更新版与组合数公式版的结果全部一致,0 处不一致。

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

分步演示:原地从右往左更新

下面的演示用 rowIndex=5:数组从全 1 开始,每一轮 i 都从右往左更新,j 从 i-1 递减到 1。蓝色是本轮正在更新的位置,绿色是本轮已经更新完成、进入下一轮的位置。每一帧都是把上面的解法在这组输入上真实执行记录下来的——注意每一轮内部更新顺序是从右往左,这正是本文"陷阱"一节的核心。

O(k)空间生成第rowIndex行:原地从右往左更新
本轮正在更新的位置本轮已经更新完成

    参考与说明

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