LeetCode 129. 求根节点到叶节点数字之和:两种解法详解(栈+递归)

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. 核心思路

用「栈」模拟二叉树的深度优先遍历(先根后左右),栈中存储「当前节点」和「当前路径已组成的数值」。遍历过程中,每遇到一个节点,就更新当前路径的数值;当遇到叶节点时,就把当前路径的数值加入总和,直到遍历完所有路径。

步骤拆解:

  1. 边界判断:如果根节点为空,直接返回 0;

  2. 初始化栈:将根节点和根节点的数值(初始路径数值)压入栈;

  3. 循环遍历栈:只要栈不为空,就弹出栈顶元素(当前节点+当前路径数值);

  4. 判断是否为叶节点:如果是,就把当前路径数值加入总和;

  5. 非叶节点处理:如果有右孩子,先压入右孩子(因为栈是先进后出,保证左孩子先遍历),同时计算右孩子对应的路径数值(当前数值×10 + 右孩子值);同理处理左孩子;

  6. 遍历结束,返回总和。

2. 完整代码(TypeScript)
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. 核心思路

递归的本质是「自顶向下传递路径数值,自底向上累加总和」。定义一个辅助函数,接收「当前节点」和「当前路径已组成的数值」,递归遍历左右子树,当遇到叶节点时,将当前路径数值加入全局总和,否则继续向下传递更新后的路径数值。

步骤拆解:

  1. 边界判断:如果根节点为空,直接返回 0;

  2. 初始化全局总和 sum(用于存储所有路径数值的和);

  3. 定义辅助函数 helper(node, val):

    • 如果当前节点为空,直接返回(递归终止条件之一);

    • 如果当前节点是叶节点,将 val 加入 sum,返回(递归终止条件之二);

    • 非叶节点:递归处理左孩子,传递的路径数值为 val×10 + 左孩子值;同理递归处理右孩子;

  4. 调用辅助函数,初始参数为 root 和 root.val;

  5. 返回 sum。

2. 完整代码(TypeScript)
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),只是实现方式不同。二叉树的很多路径问题(比如求路径和、路径是否存在)都可以用这两种思路解决,掌握这道题,能举一反三搞定一类题。

相关推荐
Joker Zxc2 小时前
【前端基础(Javascript部分)】1、JavaScript的基础知识(组成、应用、编写方式、注释)
开发语言·前端·javascript
郝学胜-神的一滴2 小时前
Python中的“==“与“is“:深入解析与Vibe Coding时代的优化实践
开发语言·数据结构·c++·python·算法
HelloReader2 小时前
Tauri 项目结构前端壳 + Rust 内核,怎么协作、怎么构建、怎么扩展
前端
HelloReader2 小时前
Tauri 前端配置把任何前端框架“正确地”接进 Tauri(含 Vite/Next/Nuxt/Qwik/SvelteKit/Leptos/Trunk)
前端
流云鹤2 小时前
动态规划01
算法·动态规划
上单带刀不带妹2 小时前
【Axios 实战】网络图片地址转 File 对象,附跨域解决方案
开发语言·前端·javascript·vue
岱宗夫up2 小时前
【前端基础】HTML + CSS + JavaScript 基础(一)
前端·css·html
SuperEugene2 小时前
前端模块化与 import/export入门:从「乱成一团」到「清晰可维护」
前端·javascript·面试·vue
abyyyyy1232 小时前
oj题目练习
java·前端·数据库