news 2026/9/26 13:40:15

LeetCode 513找树左下角的值:BFS层序遍历与DFS递归深度全解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LeetCode 513找树左下角的值:BFS层序遍历与DFS递归深度全解

LeetCode 513这道题,我的建议是每一位刷二叉树专题的人都要把它做透。题目名字叫《找树左下角的值》,给定一棵二叉树,返回最后一层最左边的节点值。它难度不高,却在一道题里同时踩中了层序遍历、递归深度、边界处理三个考点,非常适合当作二叉树知识点的总结题来对待。这篇内容不会只丢一个能过的答案,我会把两种主流解法、背后的思考路径、刷题时容易踩的坑,以及从这道题延伸出去的一串经典题目,一次讲透。

如果你正准备面试,或者在系统刷LeetCode热题100和二叉树专项,这篇正好对路。新手可以直接照抄代码加注释跑通,有经验的也能在"反向入队法""递归深度控制"这些细节里找到一点新东西。

1. 题目到底在问什么:先拆解"左下角"三个字

1.1 一句话说清题目

我给你翻译成人话:一棵二叉树从上到下分了很多层,你要找到最深的那一层,然后在那一层里找出最靠左的节点,返回它的值。

题目原描述里经常出现一个词——"bottom left"。LeetCode 513的原题是英文的,中文社区翻译过来有叫"找树左下角的值",也有叫"最底层最左边的值",都指向同一个定义。注意这个定义有两个限定条件:第一,必须是最后一层;第二,必须是这一层里最左边的节点。两个条件缺一不可。

有一个隐藏性质值得多说一句:二叉树的最后一层节点,其实全部都是叶子节点。因为只要某个节点还有孩子,那孩子所在的层一定比它更深,它就不可能是最后一层的节点。所以这道题本质上就是"找到最深层的那一组叶子,取最左边的一个"。这个性质对理解DFS解法里的叶子判断会很有用。

1.2 第一层坑:左下角不等于一路向左

很多人看到"左下角"三个字,第一反应是"从根节点一路往左走到底不就行了"。这个直觉在大部分入门二叉树文章里是被反复灌输的,但放到这道题上,它是错的。

我举个例子:

1 / \ 2 3 \ 4

如果按"一路往左"走,你会得到节点2,但这棵树的最后一层是深度2的那一层,这一层只有一个节点4,所以左下角的值应该是4,不是2。问题就出在"最左"必须满足"最深"这个前提。

再看一个反例:

1 / \ 2 3 / \ 4 5

这棵树的最后一层是深度2,节点4和5都在这一层,左下角是4。一路往左走恰好也能到4,属于碰巧。但前面那个例子已经说明:没有深度信息加持的"向左"路径,根本不能保证到达最后一层。

这也是为什么这道题被很多老师拿来当"知识点总结题"的原因——它逼着你把"层次""深度""最左"这三个概念分开想清楚,然后再缝合到一起。

2. BFS层序遍历:最稳的一版解法

2.1 层序为什么是天然答案

BFS层序遍历的逻辑是:从上到下,一层一层访问,同一层内部按照从左到右的顺序处理。既然题目要的就是"最后一层第一个节点",层序遍历几乎就是照着答案写的——你只需要在遍历过程中记录每一层的第一个节点,最后一层记录到的那个就是结果。

如果不用显式地记录"每层第一个",还有一种更巧妙的做法:既然我们最终要的是"最后访问的那个节点",那只要调整同层节点的入队顺序,让每一层都从右往左遍历,那么整棵树遍历过程中最后被访问到的节点,天然就是最后一层最左边的节点。这就是很多题解里说的"反向入队法"。

这两种思路,一种直白,一种精巧,我都会给出完整实现。

2.2 写法一:记录每层第一个出队节点(Python)

最常见的层序遍历版本,用队列保存当前层的节点,每层处理完后进入下一层。关键点在于:每层循环里,第一个从队列里弹出的节点,就是这一层最左边的节点。

from collections import deque class Solution: def findBottomLeftValue(self, root): if not root: return -1 queue = deque([root]) leftmost = root.val while queue: size = len(queue) for i in range(size): node = queue.popleft() if i == 0: leftmost = node.val if node.left: queue.append(node.left) if node.right: queue.append(node.right) return leftmost

这里for i in range(size)里的size必须在进入循环前用len(queue)保存下来,不能写成for i in range(len(queue)),原因后面第4节会专门讲。leftmost变量是不断被覆盖的,每处理完一层,它就更新成那一层最左边的值,所以循环结束时它自然就是最后一层最左边的值。

这个版本比较符合直觉,面试时先在白板上写这个,基本不会出错。

2.3 写法二:反向入队法(Python + Java)

第二种写法非常飘逸,也是很多官方题解喜欢用的版本。核心就一句话:每次出队一个节点,先把右孩子入队,再把左孩子入队,整个遍历顺序就会变成"同一层从右往左,层与层从上往下",最终最后一个出队的节点就是左下角。

from collections import deque class Solution: def findBottomLeftValue(self, root): queue = deque([root]) node = root while queue: node = queue.popleft() if node.right: queue.append(node.right) if node.left: queue.append(node.left) return node.val

为什么出队顺序会变成从右往左?我画个最简单的例子:

1 / \ 2 3 / \ / \ 4 5 6 7

队列初始是[1]。弹出1,把右孩子3放进去,再把左孩子2放进去,队列变成[3, 2]。接下来弹出3,放入它的右孩子7和左孩子6,队列变成[2, 7, 6]。再弹出2,放入5和4,队列变成[7, 6, 5, 4]。你看,第二层节点从右往左弹出,第三层也是从右往左弹出,最后一层最左边的是4,它在最后才被弹出。

这个版本少了一个leftmost变量,逻辑更紧凑。Java版本同样简洁:

class Solution { public int findBottomLeftValue(TreeNode root) { Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); TreeNode node = root; while (!queue.isEmpty()) { node = queue.poll(); if (node.right != null) queue.offer(node.right); if (node.left != null) queue.offer(node.left); } return node.val; } }

我实测下来,这个解法在LeetCode 513上时间和内存表现都很好,代码也少。但有个前提:你要能熟练解释"为什么最后一个出队的节点就是左下角",如果面试官追问而你自己都没画过例子,容易被带崩。

2.4 BFS的时间与空间复杂度

BFS的时间复杂度是 O(n),n是二叉树节点个数,因为每个节点都恰好入队出队一次。空间复杂度取决于队列在任意时刻最多能装多少节点,也就是二叉树的最大宽度。最坏情况下,比如一棵满二叉树,最后一层节点数约 n/2,所以空间复杂度最坏是 O(n)。

这个复杂度水准对 LeetCode 513 的数据规模(节点数最多一万多)来说完全没压力。但如果你要写递归版,复杂度的解释方式就完全不同了,见下一节。

3. DFS递归解法:手动补上"深度"这个维度

3.1 核心思路:首次到达该深度的节点就是最左

DFS(深度优先搜索)天然不带"层"的概念,它关心的是"深度"。好在我们可以用递归函数的参数把深度逐层传下去,然后在遍历过程中记录一个"见过的最大深度"。

这套逻辑的关键在于一个判断:每当某个节点所在的深度大于当前记录的最大深度,就更新答案。因为DFS先走左子树再走右子树,所以每个深度第一次被"解锁"的时候,访问到的那个节点一定是这一层最左边的节点。之后同层的其他节点再访问到时,深度不再大于已记录的最大深度,也就不会覆盖答案。

用生活化的类比:你拿着一个本子往地下停车场一层层走,每到一个新楼层(第一次见到的深度),就在本子上写下这一层第一个看到的车位号。之后在这一层看到其他车位一律不记。等你把整栋楼走完,本子上最后一条记录,就是最深一层第一个看见的车位。

3.2 Python实现与逐行注释

class Solution: def findBottomLeftValue(self, root): self.max_depth = -1 self.answer = 0 def dfs(node, depth): if not node: return # 核心判断:首次到达当前深度 if depth > self.max_depth: self.max_depth = depth self.answer = node.val # 先左后右,保证同一深度优先访问最左节点 dfs(node.left, depth + 1) dfs(node.right, depth + 1) dfs(root, 0) return self.answer

这里self.max_depth初始化为 -1,是因为根节点的深度是0,而 -1 小于0,所以根节点一定会在第一轮被记录为答案。如果你习惯让根节点深度是1,那max_depth就初始化为0,逻辑一模一样。

self.answer会被不断覆盖,只有当depth > self.max_depth时才更新。整个DFS走完后,answer保存的就是最深一层最左边的节点值。

3.3 为什么必须"先左后右"

如果把代码改成先递归右子树、再递归左子树,会得到什么?答案会变成"右下角"。因为每个深度第一次被访问到时,是从右子树路径到达的,记录的自然是这一层最右边的节点。

这个顺序极其容易被忽略。我自己第一次写这个解法时,凭直觉先写了dfs(node.left)再dfs(node.right),刚好对了。但后来想改成"从右往左"版本的题目时,发现只要把两行顺序换一下就行。这说明"先左后右"不是代码习惯,而是跟答案绑定的必要条件。

如果你想在面试里展示自己对这个细节的理解,可以主动跟面试官说一句:"这里递归顺序是关键,先访问左子树保证了更新条件触发时记录的是最左节点。"

3.4 递归深度的边界问题

DFS递归在刷题时有个隐患:递归深度。Python默认递归深度上限是1000层,LeetCode上二叉树的深度最坏可以到一万甚至更多。如果测试用例给了一棵链状树(每个节点只有左孩子),DFS递归到第1001层就会抛出RecursionError。

我在实际做这道题时没有踩到递归深度的坑,因为513的测试数据对DFS是友好的。但如果是自己扩展练习,或者把代码搬到一个节点特别多的场景,BFS迭代版本就不会有这个问题。这是面试时选择BFS的一个正当理由:你不仅能给出正确的答案,还能解释为什么在极端输入下BFS比DFS更稳。

如果面试官坚持要你写DFS,也可以给出一个补充方案:显式用栈做迭代版DFS。但坦白说,迭代DFS模拟递归的"先序、中序、后序"过程比BFS麻烦,不如直接用BFS。

4. 刷题现场常见的四个错误

4.1 队列size提前保存的失误

第一个高频错误发生在BFS写法里。新手容易写出下面这种代码:

while queue: for i in range(len(queue)): node = queue.popleft() ...

表面上看,range(len(queue))在每次循环开始时计算一次长度,好像没什么问题。但在Python里,range的参数只在生成range对象时求值一次,之后循环次数就固定了。所以这种写法其实是能用的,它等于在进入for循环前把当时的len(queue)快照了下来。

真正出错的是Java或C++里常见的写法:

while (!queue.isEmpty()) { int size = queue.size(); for (int i = 0; i < queue.size(); i++) { // 每次循环都重新取值 TreeNode node = queue.poll(); ... } }

这里i < queue.size()会在每次循环时重新计算size,而poll操作又让队列变小,导致循环次数不足,一层没处理完就提前进入下一层。我见过不少人在IDE里单步调试才找到这个问题。

所以代码规范要统一:进入循环前先把size保存到一个局部变量里,循环条件用这个局部变量判断。不管什么语言,这个习惯都不亏。

4.2 深度变量更新时机不对

DFS版本的坑主要在成员变量的更新时机。如果你把max_depth和answer作为局部变量放进递归函数,就会遇到"每次递归栈都是新变量"的问题。

比如有的人会写:

def dfs(node, depth): max_depth = -1 answer = 0 ...

这样每次递归进来都会把max_depth重新初始化为-1,等于什么也没记录,最终返回一个错误答案。正确的做法是:要么把max_depth和answer定义成类的成员变量(用self.前缀),要么在嵌套函数里用nonlocal声明:

def findBottomLeftValue(self, root): max_depth = -1 answer = 0 def dfs(node, depth): nonlocal max_depth, answer ...

nonlocal在LeetCode的编辑器里是支持的,但很多面试场景下,考官更习惯看到self.max_depth这种成员变量的写法,不容易产生歧义。

4.3 把"左下角"当成了"最左叶子"

前面第1节提过,最后一层的节点全部都是叶子。但这不意味着你可以直接写"找到最左的叶子"。

举个反例:

1 / 2 / 3

这棵树只有一条左链,最后一层的节点是3,它确实是最左叶子。如果题目改成"返回整棵树最左边的叶子节点,不论深度",答案也是3,碰巧一致。但下面这棵树就不一样:

1 / \ 2 3 / 4

最后一层是节点4,左下角是4。如果按"最左叶子"找,从根节点开始,2和4是叶子,最左叶子是2,那就错了。所以判断"左"之前,必须先满足"深"这个前提。这也是DFS版本里我把更新条件设计为depth > max_depth而不是"如果是叶子就更新"的原因。

你可以在心里记一句话:这道题不是"找到最左的叶子",而是"找到最深层的首节点"。

4.4 递归栈溢出与元素取值错误

递归栈溢出前面已经提过。还有一个不太容易被发现的取值错误:如果二叉树节点值本身可以是任意整数,那不要把answer初始化为一个特定的魔法值,比如-1或0,因为如果根节点的值恰好等于初始值,会干扰判断。

比如有人写:

self.answer = -1

而整棵树的根节点值正好是-1,并且最后一层左下角也是-1,那答案碰巧对。可如果左下角是0,但代码对DFS的更新顺序理解错了,导致缓存没更新,就可能返回一个听起来很合理的错误值。正确思路是:answer初始值无所谓,反正只要正确执行DFS,它一定会在第一轮被覆盖。真正决定答案的是max_depth的比较逻辑,不是answer的初始值。

4.5 常见问题速查表

错误现象可能原因修复方案
返回结果少了一层Java遍历时循环条件没保存size先用局部变量保存queue.size()再循环
DFS返回根节点的值max_depth定义在递归函数内部被反复重置改用self成员变量或nonlocal声明
返回了最右侧节点递归顺序先右后左交换dfs递归左右子树的顺序
遇到深层树直接崩溃Python递归深度超限改用BFS迭代版本
空树或单节点树结果不对没有处理root为空,或根节点深度没初始化好判断空树返回默认值,根深度与max_depth初始值对齐

5. 一道513,串起一整片二叉树题

5.1 一题三变:右视图、最大深度、层均值

LeetCode 513最让我喜欢的一点,是它能牵出一整串经典题目,几乎可以当作一个"知识点总结专题"来刷。

如果你把BFS里记录"每层第一个节点"改成"每层最后一个节点",会得到 LeetCode 199 二叉树的右视图。如果你把DFS里depth > max_depth的更新逻辑单独抽出来,只记录最大深度,不做节点值记录,会得到 LeetCode 104 二叉树的最大深度。如果把层序遍历里的每个节点值累加并除以每层节点数量,会得到 LeetCode 637 二叉树的层平均值。

也就是说,513这道题其实是这组题目里最综合的一题,它同时涉及层序、深度、每层首节点三个维度。你在吃透513之后再去刷199和104,会感觉那些题目几乎是"删掉某些代码"就能得到的。

5.2 变体一:要求返回"左下角"的完整路径

面试官可能在513基础上追加一问:不光返回左下角的值,还要返回从根到左下角节点的路径。这要求DFS在记录答案时,顺便记录当前递归路径。

class Solution: def findBottomLeftValue(self, root): self.max_depth = -1 self.answer_path = [] def dfs(node, depth, path): if not node: return path.append(node.val) if depth > self.max_depth: self.max_depth = depth self.answer_path = path[:] # 拷贝当前路径 dfs(node.left, depth + 1, path) dfs(node.right, depth + 1, path) path.pop() dfs(root, 0, []) return self.answer_path

注意path[:]这一步不能省。直接赋值self.answer_path = path的话,后面递归回溯时path.pop()会把已经记录的路径改坏,最终答案变成空列表。我一开始就是没拷贝,输出结果一直对不上,加上[:]就好了。这个坑非常典型,值得单独记一笔。

5.3 变体二:从二叉树BFS到矩阵BFS

BFS不只是二叉树能用。LeetCode 994腐烂的橘子、LeetCode 1162地图分析,都是在二维矩阵上做BFS。它们的共同点是:每次扩散一层(分钟/步数),经过几次扩散后统计结果。513里的"层"在矩阵题中对应"轮"或"分钟",队列的核心思想完全一致。

如果513你已经写得非常顺,我建议立刻去刷一遍994腐烂的橘子。你会发现它本质上就是"从多个起点同时开始做BFS,计算扩展到全图需要多少层"。这种从一个点到多个点、从树到图的跳跃,是刷题进阶时特别重要的思维训练。

最后再分享一个我自己刷题时的习惯:像513这种题,我一般先写BFS版本保底,保证至少能过,再写DFS版本验证自己对递归深度的理解。面试时如果时间够,两种解法都讲一遍,重点对比它们的空间复杂度差异,这比单纯背答案要加分得多。但如果你时间紧,优先掌握BFS反向入队法,因为它代码最短,最不容易出错,也最容易在高压环境下临场写对。

刷题不在多,而在把一道题真正吃透。513就是那种值得反复回味的题目——从它出发,二叉树的核心考点能串起一大片。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 13:39:37

libcurl与OpenSSL开发库配置指南:32位和64位选型、编译与排错

简介&#xff1a;这份资源面向需要在 Windows 平台进行 HTTPS 网络通信开发的 C/C 程序员&#xff0c;提供实测可用的 libcurl 与 OpenSSL 动态开发库&#xff0c;同时包含 32 位与 64 位两个版本&#xff0c;可解决跨架构编译时库文件不匹配、链接失败等常见问题。压缩包共 34…

作者头像 李华
网站建设 2026/9/26 13:39:32

Atlas 300V 24G部署YOLO实战:推理加速卡定位与模型转换避坑指南

我一说“Atlas”&#xff0c;圈内人一般会先想到两个东西&#xff1a;一个是数据库中间件&#xff0c;另一个就是昇腾的AI硬件平台。从“atlas部署yolo”和“atlas 300v 24g 是运算加速卡吗”这两个热搜词来看&#xff0c;大家问的基本就是后者&#xff0c;而且是买完卡之后第一…

作者头像 李华
网站建设 2026/9/26 13:38:58

小红书运营技能包:基于Claude Code的可插拔技能库实践指南

简介&#xff1a;一套完整的小红书运营技能方案&#xff0c;共一百三十九项技能插件&#xff0c;面向内容创作者、品牌商家与个人运营者&#xff0c;覆盖选题策划、笔记撰写、图片视频编辑、账号形象打造、评论私信互动、直播话题挑战、数据复盘与店铺推广销售等全链路操作&…

作者头像 李华
网站建设 2026/9/26 13:38:53

基于SpringBoot的博客论坛系统实战:从数据库设计到JWT鉴权与Redis缓存

很多人把基于Java SpringBoot的博客论坛系统当成一个“烂大街”的课设选题&#xff0c;我最初也这么认为。直到自己把一个带源码、文档、运行视频和讲解视频的完整博客论坛系统从零做完&#xff0c;才发现这个项目远比想象中更能检验一个Java开发者的综合能力——它不只是一堆增…

作者头像 李华
网站建设 2026/9/26 13:38:36

mingw-w64完整包解压即用:解决gcc报错与Windows C/C++环境配置

简介&#xff1a;这份 mingw-w64 完整包面向在 64 位 Windows 上进行 C/C、Go 等语言开发的用户&#xff0c;尤其适合被 gcc 报错、路径配置或依赖缺失困扰、希望跳过繁琐环境搭建的开发者。包内共约 2000 个文件&#xff0c;以 h 头文件、a 静态库、py/pyc/pyo 脚本与字节码、…

作者头像 李华