☰
二叉树右视图全解:BFS层序取末位与DFS先右后左
2026/10/12 8:43:19 网站建设 项目流程

说个实在话,二叉树的右视图这道题,是我在刷 LeetCode 时反复做了三遍才真正吃透的一道题。题面特别简单:给你一棵二叉树,让你想象自己站在树的右侧,返回从上到下看到的节点值。很多人第一反应是“那我一直往右走不就行了”,结果一提交就发现测试用例过不去。其实这道题的核心不是“向右”,而是“每层最右边的节点”。只要你理顺了这一点,BFS 的层次遍历方案基本就水到渠成了。

我比较推荐把这道题当作学习二叉树遍历的“过渡题”来练。它不像前中后序遍历那样需要专门记忆递归/迭代模板,也不像重建二叉树那样需要复杂的索引推导,但它把“层”这个概念拉得很清楚:一层一层地遍历,每层只取一个节点。等你能独立写对这道题的 BFS 解法,再去做层序遍历、二叉树的深度、树的最大宽度这些题,思路会顺畅很多。这篇文章我就把 BFS 解法、DFS 解法、常见运行时错误和变体扩展一次讲透,争取让你读完就能直接通刷这一类题。

1. 题目到底在问什么:右视图不是玄学

1.1 站在右侧看到的是什么

先看一个最基础的例子。如果一棵树是这样的:

1 / \ 2 3 \ \ 5 4

站在树的右侧看,你能看到哪些节点?第一层唯一一个节点是 1,看到;第二层从右侧看,2 被 1 挡住,但 3 在右边,看到;第三层,5 和 4 都在各自父节点的右侧,这时候视线从右边扫过去,4 比 5 更靠右,所以看到的是 4。最终右视图是[1, 3, 4]。

注意一个关键点:5 在第二层右子树里,但第三层还有一个更靠右的 4,所以你看不到 5。如果把 4 删掉,答案就变成[1, 3, 5]。这说明了右视图的本质:每一层只能有一个节点进入结果,且必须是这一层里“最靠右”的那个。这里的“最靠右”不是指路径上的最后一个右孩子,而是指在层序遍历顺序里最后被访问到的一个节点。

还有一个容易错的细节:如果左侧子树特别深,那么最后一行看到的可能是左侧子树里的节点。比如根节点只有左子树,没有右子树,那么站在右边看,最后一层能看到的就是左子树最深的那一层最右侧节点。所以“只往右走”是肯定错的,要遍历完整棵树才能保证每层都没漏。

1.2 BFS 为什么是第一直觉

右视图要求按层取节点,而 BFS(广度优先搜索)天然就是按层遍历二叉树的。队列里先放根节点,然后一层一层往外弹:处理当前节点时,把它的左右孩子塞进队列尾部。每一轮循环开始时,如果我们知道当前队列里有多少个节点,就等于知道了这一层的宽度,遍历完这些节点之后,队列里剩下的正好是下一层的全部节点。

这种“一层一个窗口”的结构,就是 BFS 解决树的层次类问题的标准姿势。右视图只是在这个窗口里多做了一个动作:记录每一层最后一个被弹出的节点。说白了,BFS 负责“把每一层剥出来”,我们负责“在每层结束前截取一个值”。所以这道题用 BFS 做,几乎是零思考成本。

1.3 先纠正一个题号的小误解

题目标题写着“52”,其实 LeetCode 上这道题的编号是 199,题名是 Binary Tree Right Side View。数字可能是某个刷题网站的排序编号,或者是自己整理题库时的手写编号,并不影响算法本身。我的经验是:刷题时不要迷信题号,重要的是把题目特征提炼出来——这道题的关键词就是“二叉树 + BFS + 每层末位节点”。你甚至可以自己给它起一个索引叫“层末节点”,以后每次遇到类似题目,都往这个索引上靠。

2. BFS 解法拆解:逐层推进,取每层最右

2.1 核心思路:队列记录整层节点

BFS 用队列实现,但不同语言的队列写法差异很大。Python 里最标准的是collections.deque,它支持 O(1) 的popleft()。如果你用list.pop(0),虽然也能跑,但底层是 O(n) 的数组搬移,在树节点数量到达十万级的时候,性能差距会非常明显。本质上,队列在每一轮循环里承载的是“当前层所有节点的集合”,我们只要精确控制循环次数等于当前层节点数,就能做到层与层之间不串味。

这里有个容易写错的点:如果你直接把while queue:里弹一个节点、同时把左右孩子入队,没有任何计数器,那这其实是“平坦遍历”,根本不知道哪几个节点是同一层的。要按层取节点,必须在外层 while 循环里先记录size = len(queue),再写一个内层 for 循环恰好迭代 size 次。每一轮 for 循环结束,queue 里就只剩下一层的新节点。

2.2 代码实现与逐行注释

下面这份 Python 代码我本地实测过,可以在 LeetCode 上直接通过。我刻意把注释写得细一些,方便你对照。

from collections import deque def rightSideView(root): if not root: return [] result = [] queue = deque([root]) while queue: size = len(queue) # 当前层的节点数量 for i in range(size): node = queue.popleft() # 从左到右弹出当前层节点 if i == size - 1: # 当前层最后一个节点,就是右视图该看到的点 result.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) return result

为什么要把result.append(node.val)放在if i == size - 1里,而不是在 for 循环外取queue[-1]?因为 for 循环结束时,queue 里可能已经混入了下一层的节点,queue[-1]不一定是当前层的最后一个节点。更稳妥的做法就是在弹出当前层最后一个节点的那一刻,立刻记录值。这属于“当场记录比事后推断更可靠”的典型例子,写代码时养成这种习惯能省很多调试时间。

如果你想写得再精简一点,也可以在 for 循环里不判断i == size - 1,而是用一个变量last_val每轮更新,只保留最后一个弹出的值,等内层循环结束后统一 append。本质上一样,只是代码看起来更啰嗦。我建议还是用第一种写法,判断清晰,后续扩展左视图时,把判断条件改成i == 0就行,一行都不用加。

2.3 复杂度分析:时间 O(n),空间 O(w)

时间复杂度是 O(n),因为每个节点只会被访问一次:进入队列一次、弹出一次、左右孩子检查一次。无论树怎么变,节点总数 n 不会因为算法而增加工作量。很多题目埋的坑在于重复遍历,比如递归时每个节点被处理两次之类的,但这个 BFS 方案每个节点正好进队一次,绝对没有多余操作。

空间复杂度稍微讲究一点。最差情况是满二叉树,最后一层有大约 n/2 个节点,队列里同时可能存着这么多节点,所以空间是 O(n)。如果更精确一点写,可以写成 O(w),w 是树的最大宽度。普通情况下,宽度远小于节点数,面试时说 O(w) 会显得你更懂行。还有一种很特殊的退化情况:树退化成一条链表,每层只有一个节点,那么队列始终最多只有一个节点,这时候空间复杂度实际上是 O(1)。不过别把这个当作普遍结论,因为你没法保证所有测试用例都这么友好。

2.4 为什么不能只一路向右走

我见过不少人拿到这道题就开始写:

res = [] while root: res.append(root.val) root = root.right

这种写法对某些恰好每条路径都偏右的树能蒙对,比如根节点只有右孩子,或者每个节点的右子树都比左子树深。但一旦树里有“左侧深节点”,就立刻出错。举个例子:

1 / \ 2 3 / 4

如果只往右走,你会拿到[1, 3],但站在树右侧看,第三层能看到 4,正确答案是[1, 3, 4]。原因很简单:4 虽然整体在左子树,但站在右侧视线里,第三层没有其他节点比它更靠右,所以它必须出现在右视图里。

理解这一点后,你会意识到右视图问题的真正难度不是“选右”,而是“不漏层”。只要某一层有节点,不管它在根的左边还是右边,都要在结果里占据一个位置。而 BFS 对每层都做了全量扫描,所以它能保证不漏。这也是为什么 BFS 方案在正确性上天然优于“贪心往右走”的思路。

3. DFS 也能做:先右后左的“深度优先遍历”

3.1 核心思路:第一个到达该深度的节点就是答案

BFS 是正路,但面试时如果你能顺带提一嘴 DFS 版本,会显得你对树的遍历理解更立体。DFS 的思路是:我们不需要一次性拿到整层节点的集合,只需要按“从右到左”的顺序去遍历树,那么每一层第一个访问到的节点,一定是右视图里该层该出现的节点。这种思路的核心在于“访问顺序比遍历方式更重要”。

所以遍历顺序必须是:先根,再右子树,最后左子树。这样在深度为 0 的第一层,根节点第一个被访问,记录下来;深度为 1,右孩子先被访问,记录下来;如果右子树在这个深度没有节点,才轮到左子树的节点。换句话说,DFS 利用“先右后左”的顺序,把每层结果提前占位了。

3.2 递归实现与深度记录

递归做法里需要一个深度参数来记录当前到了第几层。维护一个结果列表result,当depth == len(result)时,说明这个深度还没有被“占位”,当前节点就是右视图的节点。代码如下:

def rightSideView(root): result = [] def dfs(node, depth): if not node: return if depth == len(result): result.append(node.val) # 第一个到达该深度的节点,必然是最右侧的 dfs(node.right, depth + 1) # 先右 dfs(node.left, depth + 1) # 后左 dfs(root, 0) return result

这个递归只有三行核心逻辑,但每一步都很关键。首先是先右后左的顺序,绝对不能反。反了就变成左视图了。其次是depth == len(result)这个判断,它巧妙利用了 result 的当前长度来当作“最深已记录层数”,不需要额外建哈希表来记录每层状态。最后是递归终止条件not node,这在大规模递归时也很重要,漏了它必然报空指针错误。

细节上,递归深度等于树的高度。如果树退化成一条 5000 层的链表,Python 默认递归深度限制大约是 1000,直接就会抛RecursionError。这里不是算法错,而是语言运行时的限制。要规避的话,可以手动把递归改成栈迭代,或者用sys.setrecursionlimit临时调高深度。不过面试场景里,最好还是主动提一句“递归有爆栈风险,工程实现我倾向于 BFS”,既显得你懂,又不会在极端数据下翻车。

3.3 BFS 和 DFS 怎么选

如果这是一道面试题,我给你的建议是:先答 BFS,再补充 DFS。BFS 是正解,因为它最直观、最好解释;DFS 是加分项,能展示你对树的遍历顺序有深入理解。

分析一下两者的实际差异。BFS 严格要求逐层访问,每个节点入队出队一次;DFS 是递归深入,如果树特别深会有栈溢出的风险,而且递归本身也有函数调用开销。从代码量看,DFS 的递归版本更短,但 BFS 的迭代版本更稳。从工程角度看,BFS 更推荐,因为你可以把层序模板直接迁移到其他问题里。

一个小的经验是:如果题目要“返回一个列表”,用 BFS 基本没错;如果题目要“判断是否存在某种路径”,DFS 往往更直接。右视图属于“按层收敛”的问题,BFS 永远是主答案。

4. 写二叉树程序为什么总报运行时错误

4.1 空指针:最常见的元凶

几乎每一个初学者写二叉树代码都会遇到AttributeError: 'NoneType' object has no attribute 'left'。这个错误在 Python 里叫属性错误,在 Java/C++ 里就是经典的空指针异常。本质上就一个原因:你没有判断一个节点是否为 null 就尝试访问它的孩子或值。

比如 BFS 里,如果你在循环体外先queue = deque([root]),但 root 本身是 null,那么第一轮node = queue.popleft()拿到的就不是节点,而是空值。就算你第一轮躲过去了,如果你在入队时不判断node.left是否为 null,而是直接queue.append(node.left),那么队列里会混入空节点。下一轮循环一旦弹出一个空节点,程序立刻爆炸。

所以我的习惯是:任何涉及节点.children 的操作,先写if node.left:再写下一行。宁可多写两行判断,也不要赌测试用例不会给空节点。

4.2 死循环与爆栈:两个隐藏陷阱

BFS 的死循环通常不是因为队列不空,而是因为入队出队的逻辑导致队列里永远有节点。最常见的情况是你在遍历当前层时没有更新 size,而是直接在 while 循环里弹出节点再入队,这样的话队列会一直存在未处理的节点,程序根本退不出来。另一个有点隐蔽的问题是:如果你用list模拟队列,用了pop(0),在极大输入下虽然不会死循环,但时间会极其缓慢,最终超时。这个和死循环的报错不一样,LeetCode 上会显示 Time Limit Exceeded,但要意识到这是数据结构选型的问题,不是算法逻辑的问题。

DFS 爆栈则是另一类运行时错误。Python 默认递归深度大约是 1000,一旦树的高度超过这个值,运行时会抛RecursionError: maximum recursion depth exceeded。如果你在牛客或者其他平台用 Java/C++,对应叫 StackOverflowError。规避方法有三个:一是把递归改写成显式的栈迭代;二是提前用 BFS 方案;三是调用系统接口调大递归上限。第三条适合本地验证,不适合线上提交。

4.3 输入解析里的坑:字符串转列表的隐藏问题

LeetCode 的输入看起来是一个数组,比如[3,9,20,null,null,15,7],但平台内部已经帮你转成了 TreeNode 对象。可如果你本地写测试脚本模拟输入,就会发现一个常见的坑:直接把字符串按逗号分割再转 int,遇到"null"就崩了。比如int("null")会直接抛 ValueError。

正确的做法是写一个数组转二叉树的辅助函数,并明确约定"null"代表空节点。示例代码不太复杂,核心逻辑是用队列逐层建立节点关系。很多“本地运行正常,提交就报错”的诡异情况,其实都出在这一步,并不是你算法本身有问题。

def build_tree(arr): if not arr or arr[0] == 'null': return None root = TreeNode(int(arr[0])) queue = deque([root]) i = 1 while i < len(arr): node = queue.popleft() if arr[i] != 'null': node.left = TreeNode(int(arr[i])) queue.append(node.left) i += 1 if i < len(arr) and arr[i] != 'null': node.right = TreeNode(int(arr[i])) queue.append(node.right) i += 1 return root

这个函数里还有一个容易被忽略的点:层序遍历建树时,如果用site:xxx之类的工具去查,你会发现它的逻辑和 BFS 一模一样,队列里的每个节点都只负责接收自己的左右孩子。实际上,这就是反向使用 BFS 的过程,先有数组,再按层还原树结构。理解了这一点,你对 BFS 的理解就不只是“遍历”,而是“层次建模”了。

4.4 调试二叉树问题的三个技巧

第一招:打印队列状态。在 BFS 的 while 循环里,临时加一行print([node.val for node in queue]),能看到每一轮循环前队列里的节点值。只要对照纸上画的树,立刻能定位到层边界问题。

第二招:用最小用例验证。不要一上来就跑大样例,先用[1]、[1,2]、[1,2,3]、[1,2,null,4]这类小用例手动推演一遍。我发现至少一半的二叉树 bug 用四五个节点的树就能复现,根本不需要造大数据。

第三招:画图。很多二叉树题做不出来不是因为不会算法,而是没把树的结构画出来。把测试用例的树画在纸上,然后给 BFS 队列的每一步标序号,你会惊奇地发现,错误代码几乎每次都在某一个具体步骤上偏离预期。

5. 变体与扩展:一套 BFS 通吃侧视图系列

5.1 左视图怎么改

左视图和右视图是同一个问题,站在树的左侧看,每层最左边的节点进入结果。BFS 代码只需要改一个判断条件:把i == size - 1改成i == 0。因为 BFS 在遍历每层时,弹出第一个节点就是该层最左边的节点。DFS 版本呢,只要把递归顺序从“先右后左”改成“先左后右”就行。

所以当你理解了右视图,左视图对你来说就是送分题。我在实际刷题时喜欢把这两个题放在一起练,专门对比代码几乎一模一样的微妙变化,这样记忆特别牢固。

5.2 关于层序遍历的通用模板

右视图是层序遍历的“每层取末位”特例。如果你把每层的所有节点都存下来,那就是经典的层序遍历:

def levelOrder(root): if not root: return [] result = [] queue = deque([root]) while queue: level = [] size = len(queue) for _ in range(size): node = queue.popleft() level.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) result.append(level) return result

这个模板比右视图多了level列表,但骨架完全一样。它的作用是“一题打通一片”,二叉树的锯齿形层序遍历、二叉树的最大宽度、填充每个节点的下一个右侧节点指针,都可以在这个模板上微调。比如最大宽度题目,只需要把队列里记录节点和下标,然后算每层首尾下标的差;锯齿形层序只需要用reversed(level)翻转偶数层。这些都是右视图的平行扩展。

5.3 与二叉树的深度、搜索二叉树、线索二叉树的关系

二叉树的深度用 BFS 也很顺手:记录层数,每遍历完一层深度加一。甚至可以写成一个函数顺便在右视图里返回最大深度,因为右视图的 result 长度就是树高。如果你的面试官问“能不能再输出一下树的高度”,你直接说结果列表的长度就是高度,会是一个不错的互动点。

搜索二叉树和右视图没有直接关系,因为右视图不依赖于节点的值大小,只关注结构位置。但是搜索二叉树的特性在 DFS 中会影响遍历顺序——如果先右后左,正好按节点值从大到小遍历,这可以很自然地引出一个话题:BST 的降序遍历。如果你在面试中聊到这里,面试官往往会有意外的加分感。

线索二叉树则是一个古老的话题,它把空的左右指针利用起来,分别指向前驱和后继节点,简化中序遍历。写右视图的时候完全用不到线索二叉树,但如果你能说清“普通二叉树遍历的空间开销来自栈或队列,线索化是在结构层面省空间”,表明你对树的理解不拘泥于刷题。

5.4 这道题还能怎么考

面试官最常见的变形是:给定一棵普通二叉树,请你输出从右下角看过去的节点序列。这个“右下角”其实就是右视图的最后一个节点,本质上就是最下面一层最右侧的节点,BFS 直接处理就行。

还有一种考法是结合“N 叉树”。如果节点有任意多个子节点,右视图如何定义?答案仍然是每层最后一个节点,BFS 结构完全不变,只要把检查左右孩子改成遍历 children 列表。这种题能考你对 BFS 模板的迁移能力。

再进阶一点,考“求树的最大宽度”。这个题和右视图一样需要层边界,但要额外记录每个节点的下标。千万不要慌,核心还是层序遍历,只是把节点值换成下标计算。如果你前面把基础模板吃透了,这个变形只是多一个变量的事。

6. 实操中的一点心得体会

6.1 我练这道题时踩过的两个坑

我第一次写这道题的 BFS 时,没有记录size,直接在 while 循环里弹出节点并加入孩子,结果结果集里出现了大量重复的中间层节点。那时候我才意识到,BFS 的“层”不是队列天然具备的属性,而是需要靠计数器人为划出来的。后来我把这个经验总结成一句话:队列只负责提供下一个要处理的节点,层边界必须由代码自己维护。这句话我用在哪里都不过时。

第二个坑是在分析复杂度时把空间复杂度简化成了 O(n),结果被面试官反问了一句“能不能说说队列在满二叉树下的最大容量”。我当时愣了一下,后来才反应过来满二叉树的最后一层约有 n/2 个节点,队列里同时存这么多节点是很正常的事情。从这之后,凡是涉及 BFS 的题,我都会顺手说一句空间复杂度是 O(w),w 表示最大宽度,而不是笼统地讲 O(n)。

6.2 刷题之外的一个长期收益

说实话,这种题在真实业务开发里出现的概率不高,你不太可能在生产环境给一棵树拍“右侧照片”。但 BFS 的层次处理思想会在很多地方用到:多级评论系统的按层加载、社交网络的好友推荐层级、文件目录的逐层扫描,甚至后台任务按依赖层级调度,底层逻辑都类似。学会了维护队列中的“层边界”,你在处理这些数据时就不会只盯着单个节点而忽略整体节奏。

6.3 最后的实操建议

如果你现在准备笔试面试,我建议你从今天开始,把“二叉树的右视图”当作一道主题模板题,给自己安排三件事:1)用 BFS 实现一遍;2)用 DFS 先右后左实现一遍;3)把左视图的改动也写出来。三道代码放在一起对比,比单独刷三道新题更有效。我再给你一个练习路径:先右视图,再二叉树的最大深度,再层序遍历,再锯齿形层序,四道题全部用 BFS 层模板完成。等你做最后一题能不看模板默写时,这类“层相关”的二叉树题目基本上就再也难不住你了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询