LeetCode 中等题——129. 求根节点到叶节点数字之和,这道题是二叉树遍历的经典应用,核心考察「路径追踪」和「数值累加」,两种主流解法(迭代栈、递归)都很直观,适合巩固二叉树的遍历逻辑,今天就来详细拆解每一步思路,帮大家吃透这道题。
一、题目解读(通俗版)
题目很简单,给一棵二叉树,每个节点上都放着 0-9 的数字。从根节点走到每一个叶节点(没有左右孩子的节点),这条路径上的所有数字连起来会组成一个整数,我们需要计算所有这样的整数的总和。
举个例子:如果路径是 1 → 2 → 3,那么这个路径代表的数字是 123;如果还有另一条路径 1 → 5,代表数字 15,那最终的总和就是 123 + 15 = 138。
关键要点(避坑重点):
-
必须走到「叶节点」才算一条完整路径,中途节点不算;
-
空树直接返回 0(题目隐含边界条件);
-
路径数值的计算逻辑:每往下走一层,之前的数值 × 10 + 当前节点的数值(比如 1 → 2,就是 1×10 + 2 = 12)。
二、解题思路(两种核心解法)
二叉树的路径问题,本质是「遍历所有根到叶的路径」,同时记录每条路径的数值,最后求和。这里提供两种最常用的解法,分别对应「迭代」和「递归」,大家可以根据自己的习惯选择,两种解法的时间复杂度和空间复杂度都是 O(n)(n 是节点数)。
解法一:迭代法(栈实现,深度优先遍历 DFS)
1. 核心思路
用「栈」模拟二叉树的深度优先遍历(先根后左右),栈中存储「当前节点」和「当前路径已组成的数值」。遍历过程中,每遇到一个节点,就更新当前路径的数值;当遇到叶节点时,就把当前路径的数值加入总和,直到遍历完所有路径。
步骤拆解:
-
边界判断:如果根节点为空,直接返回 0;
-
初始化栈:将根节点和根节点的数值(初始路径数值)压入栈;
-
循环遍历栈:只要栈不为空,就弹出栈顶元素(当前节点+当前路径数值);
-
判断是否为叶节点:如果是,就把当前路径数值加入总和;
-
非叶节点处理:如果有右孩子,先压入右孩子(因为栈是先进后出,保证左孩子先遍历),同时计算右孩子对应的路径数值(当前数值×10 + 右孩子值);同理处理左孩子;
-
遍历结束,返回总和。
2. 完整代码(TypeScript)
class TreeNode {
val: number
left: TreeNode | null
right: TreeNode | null
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = (val === undefined ? 0 : val)
this.left = (left === undefined ? null : left)
this.right = (right === undefined ? null : right)
}
}
function sumNumbers_1(root: TreeNode | null): number {
if (!root) {
return 0;
}
let sum = 0;
// 栈中存储 [当前节点, 当前路径组成的数值]
const stack: [TreeNode, number][] = [[root, root.val]];
while (stack.length) {
const cur = stack.pop();
if (!cur) continue; // 防止空值报错(实际不会触发,因栈中元素都是合法节点)
const [node, val]: [TreeNode, number] = cur;
// 遇到叶节点,累加路径数值
if (!node.left && !node.right) {
sum += val;
}
// 先压右孩子,再压左孩子(保证左孩子先遍历,符合DFS顺序)
if (node.right) {
stack.push([node.right, val * 10 + node.right.val]);
}
if (node.left) {
stack.push([node.left, val * 10 + node.left.val]);
}
}
return sum;
};
3. 关键细节(避坑)
栈的压入顺序:先右后左。因为栈是「先进后出」,如果先压左孩子,弹出时会先处理右孩子,不符合我们习惯的「左→右」遍历顺序;先压右孩子,弹出时先处理左孩子,才能保证每条路径都被完整遍历。
cur 的非空判断:虽然栈中存储的都是 [TreeNode, number] 合法组合,但 TypeScript 类型检测会提示 cur 可能为 undefined,所以加一句 if (!cur) continue 规避报错(实际运行中不会执行)。
解法二:递归法(深度优先遍历 DFS)
1. 核心思路
递归的本质是「自顶向下传递路径数值,自底向上累加总和」。定义一个辅助函数,接收「当前节点」和「当前路径已组成的数值」,递归遍历左右子树,当遇到叶节点时,将当前路径数值加入全局总和,否则继续向下传递更新后的路径数值。
步骤拆解:
-
边界判断:如果根节点为空,直接返回 0;
-
初始化全局总和 sum(用于存储所有路径数值的和);
-
定义辅助函数 helper(node, val):
-
如果当前节点为空,直接返回(递归终止条件之一);
-
如果当前节点是叶节点,将 val 加入 sum,返回(递归终止条件之二);
-
非叶节点:递归处理左孩子,传递的路径数值为 val×10 + 左孩子值;同理递归处理右孩子;
-
-
调用辅助函数,初始参数为 root 和 root.val;
-
返回 sum。
2. 完整代码(TypeScript)
class TreeNode {
val: number
left: TreeNode | null
right: TreeNode | null
constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
this.val = (val === undefined ? 0 : val)
this.left = (left === undefined ? null : left)
this.right = (right === undefined ? null : right)
}
}
function sumNumbers_2(root: TreeNode | null): number {
if (!root) return 0;
let sum = 0;
// 辅助递归函数:传递当前节点和当前路径数值
const helper = (node: TreeNode | null, val: number): void => {
if (!node) return;
// 叶节点,累加路径数值
if (!node.left && !node.right) {
sum += val;
return;
}
// 递归处理左右子树,更新路径数值
if (node.left) {
helper(node.left, val * 10 + node.left.val);
}
if (node.right) {
helper(node.right, val * 10 + node.right.val);
}
}
// 初始调用:根节点,路径数值为根节点值
helper(root, root.val);
return sum;
};
3. 关键细节(避坑)
递归终止条件:必须同时判断「节点为空」和「节点是叶节点」。节点为空时直接返回(比如某节点只有左孩子,右孩子为空,递归右孩子时直接终止);叶节点时累加数值并返回,避免继续递归空孩子。
路径数值的传递:递归调用时,直接计算「当前数值×10 + 孩子节点值」,不需要额外存储路径,简洁高效。
三、两种解法对比(怎么选?)
| 解法 | 核心逻辑 | 优点 | 缺点 |
|---|---|---|---|
| 迭代法(栈) | 栈模拟 DFS,手动控制遍历流程 | 无递归栈溢出风险,适合节点多的大树 | 代码稍长,需要手动维护栈 |
| 递归法 | 递归传递路径数值,自动遍历 | 代码简洁,思路直观,容易编写 | 节点极多时可能触发递归栈溢出(JS/TS 递归深度有限) |
实际刷题中,递归法更简洁,适合面试时快速编写(只要树的深度不超过 1000,就不会溢出);如果遇到极端大的树,优先用迭代法。
四、刷题总结
这道题的核心是「二叉树路径追踪」,记住两个关键:
-
路径数值的计算:每向下一层,之前的数值 × 10 + 当前节点值(本质是十进制数的拼接);
-
叶节点的判断:必须是 left 和 right 都为 null 的节点,缺一不可。
无论是迭代还是递归,本质都是深度优先遍历(DFS),只是实现方式不同。二叉树的很多路径问题(比如求路径和、路径是否存在)都可以用这两种思路解决,掌握这道题,能举一反三搞定一类题。