杨辉三角 II 图解题解
这道题到底在问什么
- rowIndex
- 4
- 输出
- [1,4,6,4,1]
- 解释
- 第 0 行 [1],逐行往下推,第 4 行就是 [1,4,6,4,1]
最优解:为什么这么做
一句话答案:LeetCode 119 杨辉三角 II 只要第 rowIndex 行,用滚动数组:一维数组从右往左原地更新 row[j]+=row[j-1],方向反了会污染左肩。时间 O(rowIndex²)、空间压到 O(rowIndex)。
杨辉三角 II 要你返回哪一行
杨辉三角第 0 行是 [1],往下每行头尾都是 1,中间每个数等于正上方两个数相加。给你行号 rowIndex(从 0 开始数),返回第 rowIndex 行,例子 rowIndex=4 答案是 [1,4,6,4,1]。重点在附加要求:只用 O(rowIndex) 额外空间(大 O 记号,衡量用量随规模的增长),别把三角摊开存着。
为什么把整张三角存下来是浪费
按 118 的做法把每行都存进二维表,推到第 rowIndex 行,存下的数约等于三角形面积,是 O(rowIndex²) 的空间。可题目只问最后一行,前面的行都是推导路上的中转站,交答案时全用不上。为输出一行去存整张三角全是白占地方,题目把空间卡在 O(rowIndex) 就是在拦它。
一个一维数组凭什么就够用
相邻两行才有关系:这一行每个数只由上一行两个数加出来,更早的行推完就没用。既然只需上一行,就别开二维表,只拿一行数组反复覆盖——上一行的值就地改成这一行的值(原地更新,不另开新数组)。这种把二维表压成一行、拿新值盖旧值的写法叫滚动数组(只留用得上的一行)。空间从 O(rowIndex²) 掉到 O(rowIndex)。
为什么更新必须从右往左
每个数等于正上方两个数之和——上一行斜上方一个偏左(左肩)、一个偏右(右肩)。落到一维数组,新 row[j] 等于旧 row[j](右肩)加左边一格旧 row[j-1](左肩),即 row[j]+=row[j-1]。
麻烦在左边 row[j-1] 这一轮也要改。若从左往右,先把 row[1] 改成新值,等算 row[2] 读它当左肩时读到的是新值而非旧值,整行就错。从右往左则先动右边、再动左边,要读的 row[j-1] 还没被碰过,一定是旧值。
拿 rowIndex=4 把这行数组滚一遍
参考代码先把长度 rowIndex+1 的数组全填 1:[1,1,1,1,1],外层从第 2 行推起,内层从右往左更新。推第 2 行:row[1] 加 row[0] 得 2,成 [1,2,1,1,1]。推第 3 行:先改 row[2],左肩 row[1] 还是旧的 2,得 3;再改 row[1] 得 3,成 [1,3,3,1,1]。
推第 4 行:从右往左改 row[3]、row[2]、row[1]。row[3] 加左肩 3 得 4;轮到 row[2],左肩 row[1] 仍是旧的 3,3 加 3 得 6,正是正中间那个 6;最后 row[1] 加 row[0] 得 4,定格 [1,4,6,4,1] 就是第 4 行。
方向写反会怎样,rowIndex 很小要不要特判
外层逐行、内层逐列,总更新次数约是三角形面积,时间 O(rowIndex²);空间只有一个长 rowIndex+1 的数组,O(rowIndex)。方向写反是滚动数组最伤的一处:改成从左往右,左肩被这一轮新值污染,整行都坏,还不报错,只给你个看着对却错的结果。rowIndex=0 和 1 时,全填 1 后内层进不去,[1] 或 [1,1] 就是答案,不用特判。
▶ 动画逐步走查(共 24 步)——想跟着动画一帧帧对照就展开
- 3存整张三角要 O(rowIndex²) 空间。可我们只要最后一行,前面的行算完就没用了——这就是优化的突破口:能不能只留一行?
- 4关键是从右往左更新。新值 row[j] 需要旧的左肩 row[j-1]——只要先动右边、后动左边,左边还没被改,读到的就是旧值,一个数组就够了。
- 5若从左往右:先把 row[1] 改了,等算 row[2]=row[2]+row[1] 时,row[1] 已是新值,污染了结果。从右往左则保证每次读到的左肩都还是上一行的旧值。
- 6只激活第 0 格我们开一个长度 5 的数组(rowIndex+1)。一开始只有第 0 格 = 1,代表第 0 行 [1]。灰色格子是后面才会激活的位置,盯住它们怎么一格格点亮、被填出数来。
- 7激活 slot 1 = 1往下推一行,先在末尾补个 1(杨辉三角每行最右都是 1)。现在数组前两格是 [1,1] = 第 1 行。第 1 行中间没有要加的数,直接成形。
- 8激活 slot 2 = 1推第 2 行,再在末尾补 1,暂时是 [1,1,1]。但中间那个 1 还不对——它该等于上一行的左肩+右肩。下面从右往左修它。
- 91 + 1 = 2更新 row[1]:它加上左边的 row[0]=1,从 1 变 2。第 2 行成形 = [1,2,1]。注意此时 row[0] 还是旧的 1,没被动过。
- 10激活 slot 3 = 1推第 3 行,末尾补 1,临时是 [1,2,1,1]。中间两个数(下标 2、1)要修。先修右边的 row[2],再修 row[1]——顺序不能反。
- 111 + 2 = 3先更右边的 row[2]:加上左肩 row[1]=2(还是旧值!),变成 3。此刻 row[1] 仍是 2,等下一步才轮到它。
- 122 + 1 = 3再更 row[1]:加上 row[0]=1 变 3。第 3 行成形 = [1,3,3,1]。看,右边先改完左边才改,左肩读到的一直是旧值,没被污染。
- 13激活 slot 4 = 1推到第 4 行(目标行),末尾补最后一个 1,临时是 [1,3,3,1,1]。所有格子都激活了。接下来从右往左更新下标 3、2、1 三个数。
- 141 + 3 = 4最右先动 row[3]:加上左肩 row[2]=3,变 4。继续往左。
- 153 + 3 = 6row[2] 加上左肩 row[1]=3(仍是旧值),变 6——这就是 [1,4,6,4,1] 正中间那个 6。
- 163 + 1 = 4最后更 row[1]:加 row[0]=1 变 4。数组定格为 [1,4,6,4,1]——这就是第 4 行答案!整个过程只用了一个数组。
- 17答案 = [1,4,6,4,1]整个过程只用了一行数组,没存任何中间行。空间从 O(n²) 压到 O(n),和题目要求对上了。
- 18下面用一张三角表把刚才的滚动还原成整张杨辉三角,逐格填一遍,你会更清楚每个数是怎么从上一行加出来的。
- 19T[0][0] = 1把三角左对齐放进表格。第 0 行只有一个 1。注意每行左右两端都是 1,中间靠相加得来。
- 20两端都是 1第 1 行 = [1,1],全是两端的 1。
- 21两端 = 1,中间待填每行先把两端的 1 摆好(下标 0 和末尾),中间留空。下一步算中间。
- 22T[2][1] = T[1][0]+T[1][1]第 2 行中间那个数 = 上一行的左肩 1 + 右肩 1 = 2。所以第 2 行 = [1,2,1],正好对上滚动数组里那一步。
- 23两端 = 1第 3 行先摆两端的 1,中间两个空位等着由上一行 [1,2,1] 相邻相加。
- 24[1,3,3,1]第 3 行中间两个数都由上一行 [1,2,1] 相邻相加得来:1+2=3、2+1=3。第 3 行 = [1,3,3,1]。
- 25两端 = 1最后一行(答案行)先摆两端 1,中间留三个空位,由上一行 [1,3,3,1] 相邻相加填出。
- 26[1,4,6,4,1]第 4 行由上一行 [1,3,3,1] 相邻相加:1+3=4、3+3=6、3+1=4,两端补 1,得 [1,4,6,4,1]。和滚动数组算出的完全一致——这就是答案。
⚠️ 容易写错的地方
✗ 错:内层从左往右更新
✓ 对:必须从右往左(j 递减)
从左往右会先改了左肩,右边再读就是新值,结果污染
✗ 错:再开一个新数组存当前行
✓ 对:在同一数组上原地更新
开新数组就退化成 O(n²) 空间,丢了本题考点
✗ 错:内层循环边界写成 j>=0
✓ 对:j 到 1 即止(row[0] 永远是 1)
j=0 时没有左肩 row[-1],且两端恒为 1 不该改
完整代码(Python / C++ / Java)
Python
class Solution:
def getRow(self, rowIndex):
row = [1] * (rowIndex + 1) # 一维数组,先全填 1
for i in range(2, rowIndex + 1): # 从第 2 行开始往下推
for j in range(i - 1, 0, -1): # 从右往左更新中间
row[j] += row[j - 1] # 当前 += 左肩(还是旧值)
return rowC++
class Solution {
public:
vector<int> getRow(int rowIndex) {
vector<int> row(rowIndex + 1, 1); // 一维数组,全填 1
for (int i = 2; i <= rowIndex; i++) {
for (int j = i - 1; j >= 1; j--) { // 从右往左
row[j] += row[j - 1];
}
}
return row;
}
};Java
class Solution {
public List<Integer> getRow(int rowIndex) {
int[] row = new int[rowIndex + 1];
Arrays.fill(row, 1); // 一维数组,全填 1
for (int i = 2; i <= rowIndex; i++) {
for (int j = i - 1; j >= 1; j--) { // 从右往左
row[j] += row[j - 1];
}
}
List<Integer> ans = new ArrayList<>();
for (int v : row) ans.add(v);
return ans;
}
}复杂度
时间复杂度
O(rowIndex²)
外层逐行、内层逐列,总更新次数约是三角形面积
空间复杂度
O(rowIndex)
只用一个长 rowIndex+1 的数组,没存整张三角
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 杨辉三角 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
杨辉三角 I(LeetCode 118)和这道 II 差在哪?+
118 要你返回从第 0 行到第 rowIndex 行的整张三角,那就得老老实实把每一行都留下来,空间是 O(rowIndex²)。119 只要最后一行,前面的行都是中间产物,于是能把二维表滚成一行、就地覆盖,空间压到 O(rowIndex)。两题的递推规则一模一样,都是每个数等于上一行的左肩加右肩;区别只在要不要保留全部历史行,这一点决定了用二维表还是一维滚动数组。
除了滚动数组,还有别的办法求第 rowIndex 行吗?+
有,用组合数直接算。第 rowIndex 行的第 j 个数正好是组合数 C(rowIndex, j),而同一行里相邻两个数存在关系 C(n, j) 等于 C(n, j-1) 乘 (n-j+1) 再除以 j。从行首的 1 起步一格格乘除推过去,就能用 O(rowIndex) 时间、O(rowIndex) 空间把整行算出来,连逐行往下推都省了。拿 rowIndex=4 试一下:从 1 开始依次得到 4、6、4、1,正好是 [1,4,6,4,1]。面试里把一个数组为什么够用和这个组合数解法都答上,是加分点。
rowIndex=0 或 1 这种小输入要不要单独特判?+
不用。参考代码先把长度 rowIndex+1 的数组全填 1,再让外层从第 2 行才开始往下推。rowIndex=0 时数组是 [1],rowIndex=1 时是 [1,1],这两种情况外层循环根本进不去,内层更新一次都不执行,全填 1 的初值直接就是答案。这也是全填 1 比每行末尾再补 1 更省心的地方,它顺手把最小的两个边界兼容掉了。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 杨辉三角 II 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。