LeetCode 404 左叶子之和:二叉树遍历中的递归与迭代全解析
发布时间:2026/10/2 15:06:07来源:尧图网络
前几天在刷题群里看见有人发求助“Leetcode 404 左叶子之和明明标着简单我愣是提交错了三遍。”第一反应我以为他漏了判空点开代码才发现问题出在他把“左叶子”当成了“左孩子”来算。这不是个例评论区里踩同一个坑的人不少。LeetCode 404 是一道非常经典的二叉树遍历入门题也是LeetCode热门100题里的常客周赛前的热身练习经常能看到它。题目本身不复杂但能把“左叶子”三个字真正吃透的人其实不多。今天我就借这个机会把这道题从定义到递归、从迭代到变形题全部聊透尤其是那些编辑器不报错、逻辑上却会算错的细节保证你看完能直接照着写也能用在其他二叉树题目上。1. 题目到底在问什么从“左孩子”到“左叶子”的语义转变1.1 左叶子的精确定义很多第一眼看到“左叶子之和”的人会下意识理解成“把所有左孩子的值加起来”。这是最大的陷阱。左叶子必须同时满足两个条件它是某个节点的左孩子也就是说在父节点那一层它位于左侧。它自身没有任何孩子即left和right都为空是一个真正的叶子节点。换句话说光看节点本身不够还要看它和父节点的相对关系。一个节点哪怕没有孩子如果它是父节点的右孩子那也不叫左叶子反过来一个节点哪怕有孩子只要它是左孩子也不能算左叶子。我平时给朋友讲这个概念时喜欢打个比方想象一支足球队左叶子就像“左边锋且这轮没上场”——位置必须是左边而且这轮比赛完全没出场记录缺一不可。1.2 样例逐层拆解题目给了两个很典型的例子。第一个是二叉树[3,9,20,null,null,15,7]结构如下3 / \ 9 20 / \ 15 7在这个树里节点 9 是 3 的左孩子并且 9 的左右孩子都是空所以 9 是左叶子。节点 15 是 20 的左孩子但是 15 不是叶子它有孩子吗没有等等这里要注意15 没有孩子所以 15 其实也是叶子同时它是 20 的左孩子因此 15 也是左叶子不对让我重新看题目样例。LeetCode 404 的示例 1 是[3,9,20,null,null,15,7]答案应该是 24我印象中答案是 24。等等我是不是记混了让我仔细回忆。实际上 LeetCode 404 的示例 输入root [3,9,20,null,null,15,7]输出24解释在这个二叉树中有两个左叶子分别是 9 和 15所以返回 9 15 24。没错示例 2 是root [1]输出 0。我前面在思考时把 15 和 7 搞错了15 是叶子节点7 也是叶子节点但只有 15 是左叶子7 是右叶子所以答案是 91524。这个例子正好完美展示了“左叶子”的两个条件的必要性。示例 2root [1]只有一个根节点没有左孩子左叶子之和为 0。这里补充一个我实际踩过的坑测试用例给的是层序遍历序列但你要分析树结构时必须还原成树不能直接对着数组里的位置判断“哎下标 2 是下标 1 的左孩子所以它是左叶子”。数组里下标 2 确实是下标 1 的左孩子但下标 2 是数组里的“相对位置”不代表它在树里一定是“左叶子”因为下标 2 可能还有孩子。判断叶子必须看还原后的真实节点是否有左右子树。1.3 边界条件清单做二叉树题先列边界条件是个好习惯。这道题的边界有四个根节点为空root None左叶子之和是 0。只有一个根节点没有左孩子也没有右孩子结果是 0。根节点只有左孩子且左孩子是叶子那么结果等于该左孩子的值。根节点有左子树但左子树的根不是叶子这时要递归到左子树内部去找更深的左叶子。把这些边界想清楚比急着写代码更重要。很多时候提交报错不是算法思想错了而是边界条件少考虑了一种。2. 递归解法站在子树的肩膀上做判断2.1 递归的思考方式不要一上来就想全局二叉树类的题最忌讳一上来就想“我要怎么遍历完整棵树再统计”。正确的递归思考方式应该是只关心当前节点能决定什么剩下的事情交给递归。当前节点能决定什么答案很明确当前节点能决定“我的左孩子是不是左叶子”。如果root.left存在且root.left.left和root.left.right都不存在那root.left就是一个左叶子值应该被计入。至于左子树里更深的左叶子以及右子树里的左叶子我管不着让递归去处理。这个思路翻译成代码就是非常经典的“根节点只负责判断一层子树结果向上汇总”。2.2 完整代码与逐行解释Python 版本如下class TreeNode: def __init__(self, val0, leftNone, rightNone): self.val val self.left left self.right right class Solution: def sumOfLeftLeaves(self, root: TreeNode) - int: if not root: return 0 ans 0 # 判断当前节点的左孩子是不是左叶子 if root.left and not root.left.left and not root.left.right: ans root.left.val # 递归统计左子树和右子树中的左叶子 ans self.sumOfLeftLeaves(root.left) ans self.sumOfLeftLeaves(root.right) return ans逐行解释一下if not root空节点返回 0。这是递归的终止条件之一。ans 0当前这一层累计的答案。if root.left and not root.left.left and not root.left.right这个条件是整道题的核心。root.left存在并且它没有左孩子、没有右孩子那它就是一个左叶子。ans root.left.val把左叶子的值加进去。ans self.sumOfLeftLeaves(root.left)递归处理左子树。这里要注意如果root.left本身已经是左叶子那它的左右孩子都是空递归进去会返回 0不会造成重复计算。ans self.sumOfLeftLeaves(root.right)递归处理右子树因为右子树里也可能存在“它自己的左孩子”那些孩子从全局来看依然是整棵树的左叶子。这里有一个我遇到的易混点很多人会问如果root.left是左叶子递归它的时候返回 0我能不能直接不递归可以但没意义因为root.left的两个孩子都是空递归进去立刻返回 0。保持代码的一致性更好统一递归左右子树不容易漏逻辑。2.3 为什么递归前要先做一次左叶子判断如果不先判断直接递归会发生什么看这段错误写法# 错误的写法 def sumOfLeftLeaves(self, root): if not root: return 0 ans 0 if root.left: ans self.sumOfLeftLeaves(root.left) ans self.sumOfLeftLeaves(root.right) return ans这段代码在递归过程中完全没有“叶子判断”。递归到节点 9 时9 没有孩子返回 0但 9 作为一个左叶子它的值根本没有被加进去。所以最终结果永远是 0。结论是递归过程中必须在“父节点”这一层判断左孩子是不是叶子不能把判断扔给子递归。因为子递归只知道“我是谁”不知道“我在父节点眼里是左孩子还是右孩子”。这种信息差是这道题最核心的考点。3. 迭代解法用显式栈消除递归的隐式开销3.1 递归的隐式栈与迭代的必要性递归版本虽然简洁但底层依赖系统调用栈。如果二叉树退化成一条链递归深度可能达到节点数 N在某些环境中会触发栈溢出。所以面试时如果被追问“能不能不用递归”你需要能写出迭代版本。迭代的本质是用自己的栈模拟系统栈。每弹出一个节点就检查它的左孩子是不是左叶子然后把它的左右孩子都压入栈继续遍历。这个流程和前序遍历几乎一模一样只是多了“判断左孩子是否为叶子”的一步。3.2 迭代版本代码实现class Solution: def sumOfLeftLeaves(self, root: TreeNode) - int: if not root: return 0 stack [root] ans 0 while stack: node stack.pop() # 检查当前节点的左孩子是否为左叶子 if node.left and not node.left.left and not node.left.right: ans node.left.val if node.right: stack.append(node.right) if node.left: stack.append(node.left) return ans这段代码的思路非常直白初始化栈根节点入栈。只要栈非空弹出节点处理。判断node.left是否为左叶子是则累加。将node.left和node.right入栈继续遍历。这里入栈顺序其实无所谓因为我们是把所有节点都检查一遍先处理哪个都不影响结果。不过习惯上先压右再压左这样弹出时先处理左子树和递归的顺序保持一致。3.3 递归与迭代的复杂度对比对比维度递归解法迭代解法时间复杂度O(n)每个节点访问一次O(n)每个节点访问一次空间复杂度O(h)h 为树高最坏 O(n)O(n)栈中最多保存一层节点代码可读性高逻辑集中中需要自己管理栈栈溢出风险有树深时可能触发无使用堆内存从实际刷题角度递归写法足够通过 LeetCode 的所有测试用例。但迭代解法能帮你深入理解“遍历时如何携带额外信息”这件事很多后续题目比如二叉树的所有路径、求根到叶子的数字和都会用到这种“栈 判断”的组合套路。4. 从提交到AC真正会卡你一下的边界与易错点4.1 错误1把叶子节点和空节点混为一谈二叉树题里None和“叶子节点”是两回事。叶子节点是left和right都为空的节点空节点是None。常见的错误写法是if not root.left: return 0这行代码只判断了“左孩子不存在”完全没有判断“左孩子是否为叶子”。如果左孩子存在但不是叶子比如示例 1 中的节点 20它的左孩子 15 存在且为叶子此时 15 应该是答案的一部分但上面的错误写法会把 20 的左孩子直接忽略。正确的判断逻辑必须是if root.left and not root.left.left and not root.left.right:先确保存在再确保没有孩子两个条件缺一不可。4.2 错误2只递归左子树忘记右子树里也有左叶子这种错误很有意思因为它不是语法错误而是理解层面漏了一块编辑器完全不会提示。错误写法class Solution: def sumOfLeftLeaves(self, root): if not root: return 0 ans 0 if root.left and not root.left.left and not root.left.right: ans root.left.val ans self.sumOfLeftLeaves(root.left) return ans这版把右子树的递归给删了。测试树[3,9,20,null,null,15,7]会得到 9而不是 24。因为 15 这个左叶子在 20 的右子树里不递归右子树永远统计不到它。从二叉树的遍历角度想你只有把整棵树走完才能确定哪些节点是左叶子。任何“只走半棵树”的思路都是错的。4.3 错误3重复累计左叶子的值还有一种写法能通过示例但在某些边界用例上会重复计算# 有问题的写法 def sumOfLeftLeaves(self, root): if not root: return 0 ans 0 if root.left and not root.left.left and not root.left.right: ans root.left.val ans self.sumOfLeftLeaves(root.left) ans self.sumOfLeftLeaves(root.right) return ans乍一看好像没错但仔细想如果root.left是左叶子那么root.left.left和root.left.right都是 None递归下去会返回 0。所以在这道题里这样写其实不会重复计算。真正会重复计算的是另一种写法比如把左叶子的值加到返回结果里同时又在递归左子树时把左子树根节点的值再加一次。例如if root.left and not root.left.left and not root.left.right: return root.left.val self.sumOfLeftLeaves(root.left) self.sumOfLeftLeaves(root.right)这种写法把root.left.val加了一次然后递归左子树时如果递归函数里又判断root.left的左孩子就会在更深一层把另一个值加进来从而导致计数混乱。所以建议代码保持“当前层累加 递归调用 返回累加值”的清晰结构不要提前 return。4.4 用测试用例验证你的代码我每次写完都会在本地跑这组用例确保万无一失输入: [] 输出: 0 输入: [1] 输出: 0 输入: [3,9,20,null,null,15,7] 输出: 24 输入: [1,2,2,3] 输出: 3逐个解释空树没有节点返回 0。单节点根不是任何人的左叶子返回 0。9 和 15 是左叶子和为 24。[1,2,2,3]的树结构是1 的左孩子 2不是叶子因为它有左孩子 32 的左孩子 3 是叶子3 是左叶子所以返回 3。注意这里 1 的右孩子 2 虽然有两个孩子等等[1,2,2,3]是层序遍历还原后根 1左孩子 2右孩子 2然后 3 是左孩子 2 的左孩子。所以右孩子 2 没有孩子它也不是左叶子。答案是 3。如果这组用例都能通过代码基本没有问题。5. 左叶子之和的变体与延伸思考5.1 改一个字母右叶子之和学会了左叶子右叶子几乎不用动脑。只需要把判断条件里的root.left全部换成root.right然后递归方向对称一下。但这里有一个隐藏考点右叶子要求是“父节点的右孩子”且为叶子这和左叶子的定义在本质上完全对称。我把这个变体题当成面试时的加分题用来考察候选人是否真的理解“父节点视角下的叶子判断”而不是死背代码。5.2 更进一步求所有叶子之和所有叶子之和比左叶子更简单它不需要关心“左还是右”只需要判断一个节点是不是叶子。此时递归写法可以收敛为def sumOfLeaves(root): if not root: return 0 if not root.left and not root.right: return root.val return sumOfLeaves(root.left) sumOfLeaves(root.right)这里的关键变化是叶子判断从“父节点判断左孩子”变成了“当前节点判断自己”。这也反向说明了为什么左叶子题更难一点点因为它给叶子加上了“相对位置”维度每一个节点都必须先知道自己是左孩子还是右孩子才能决定要不要累加。5.3 拔高变体求最深左叶子的深度与值这是一个进阶版经常出现在周赛的签到题附近。要求不仅能找到左叶子还要找到深度最大的那个左叶子甚至输出它的值。推荐做法是带层级的 DFS 或 BFS。用递归的话需要额外传一个depth参数def deepest_left_leaves(root): if not root: return 0, -1 # (值, 深度) max_depth -1 result 0 def dfs(node, depth, is_left): nonlocal max_depth, result if not node: return if is_left and not node.left and not node.right: if depth max_depth: max_depth depth result node.val return dfs(node.left, depth 1, True) dfs(node.right, depth 1, False) dfs(root, 0, False) return result这段代码里的is_left参数很巧妙它记录当前节点在父节点眼里是不是左孩子。初始根节点传False因为根不是任何人的左孩子。左叶子必须满足is_left True且自身是叶子。看到这你会发现左叶子题的核心其实是一个“参数携带”技巧二叉树遍历时如何把“我在哪个方向”的信息向下传递。这个技巧在二叉树的所有路径、二叉树最大宽度等题目里都会被反复使用。5.4 从这道题想到的刷题策略最后聊聊这道题在 LeetCode 热门 100 题中的定位。它被归为简单题但面试中出现的频率并不低。原因在于“左叶子”这个概念很容易被模糊处理而面试官恰好想看看候选人能不能把需求精确翻译成代码。我自己的刷题经验是遇到二叉树题先不要急着写循环或递归先明确两件事——第一用哪种遍历方式前序、中序、后序、层序第二在每个节点上需要做什么判断。左叶子之和这道题本质是“前序遍历 父节点对左孩子的叶子判断”想通这一点代码自然就出来了。另外不要觉得简单题没用。我经常在刷题群里看到有人死磕难题反而忽略了对这类简单题的精读。但简单题里往往藏着最基础的概念比如“什么是叶子节点”“递归返回值到底该怎么逐层上传”。把这些概念练成肌肉记忆才能在做复杂题时不用分心。我个人特别喜欢这道题的另一个原因是它很适合用来练习“把题意转化为条件”的能力。看到“左叶子”立刻拆解为“是左孩子”和“是叶子”两个布尔条件这种拆解思维对后续做动态规划、图论算法同样管用。你可以试着用同样的方式去拆解题目里的其他名词比如“右路径”“根节点到叶子节点”一旦你习惯了这种拆词刷题效率会明显提升。
网站建设高端定制企业官网