给定行下标 rowIndex,只返回杨辉三角的这一行,要求只用 O(rowIndex) 的额外空间。标准做法是反复原地更新同一个数组来模拟"从上一行推出下一行",但这里有个容易踩的坑:原地更新的方向如果从左往右,算到 row[j] 时用到的 row[j-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] 还是上一轮遗留下来的旧值。
只用一个长度为 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
$ 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; } }
$ 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 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; }
$ 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; }
$ 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。蓝色是本轮正在更新的位置,绿色是本轮已经更新完成、进入下一轮的位置。每一帧都是把上面的解法在这组输入上真实执行记录下来的——注意每一轮内部更新顺序是从右往左,这正是本文"陷阱"一节的核心。