力扣hot100里有个很容易被低估的题——二叉树的右视图。我第一次刷它的时候,觉得这不就是层序遍历取最后一位吗,两分钟就交了。后来在复盘二叉树的遍历时才发现,这题用深度优先(DFS)也能写,而且写出来的代码比BFS短一半。更关键的是,它把“树的层序信息”和“递归的深度信息”这两套抽象思想放到同一道题里考,刷题量没到一定阶段的人,根本察觉不到这点。如果你正在按力扣刷题攻略刷hot100,到了树的专题卡住,或者写二叉树程序时总报运行时错误,这一篇就是为你准备的。
BFS、DFS两种主流解法我都会完整拆开讲,配合代码、复杂度分析和常见报错排查。不管是刚接触二叉树的小白,还是准备面试想快速过一遍hot100的人,看完都能直接上手复现。
1. 题目拆解:右视图到底在考什么
1.1 一句话理解题意与边界条件
题目内容不复杂:给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
这句话翻译成人话,就是每一层只取最右边那个节点。树的右视图不是你沿着右子树一路走到底能看到的所有节点,而是“每一层的最右节点”从上到下拼成的列表。
边界条件有三个,写代码前必须刻在脑子里:
- 根节点为空时,返回空列表
[],不是[None],也不要报错。 - 只有根节点时,返回
[root.val],因为站在右侧看,根节点是第一层也是唯一一层。 - 某层的“最右节点”不一定是右孩子。如果右孩子为空但左孩子存在,那么站在右侧看到的是左孩子。
第三条是最容易踩的坑。很多初学者会想当然地写“一路往右递归”,结果得到1 -> 3 -> 6这种路径,而不是真正的层序结果。你可以把右视图理解为:对每一层做水平投影,取投影后最靠右的那个点。它跟“右边界路径”完全是两回事。
1.2 为什么这题是hot100里的分水岭
hot100题里二叉树相关题目不少,但右视图是少有的“一道题同时覆盖两大遍历体系”的题目,这也是它在热题100里位置靠前的原因。
如果你只会广度优先(BFS),这题就是“层序遍历 + 取每层最后一个元素”,属于模板题。如果你只会深度优先(DFS),这题就是“递归时记录深度 + 利用首次访问时机”,属于框架题。
两种解法的时间复杂度都是 O(n),但思考方式完全不同:一个基于“队列快照”,一个基于“递归深度”。很多人刷题时只记住“右视图 = 层序取最后”,过了几天遇到变体题“左视图”“俯视图”又懵了,就是因为没在DFS视角上想明白“右视图 = 每层最先被访问到的节点”这一层。
刷完这题,你等于同时复习了:
- 二叉树的层序遍历模板
- 二叉树的深度概念(递归参数里的depth)
- DFS的访问顺序控制(先右后左还是先左后右)
它是一道“一鱼两吃”的题,认真拆透之后,后面做锯齿形层序遍历、找树左下角的值、二叉树的最大深度,都会顺很多。
2. 层序遍历(BFS)解法:站在队列上看每层末尾
2.1 核心思路:为什么BFS天然适合这题
BFS的思路非常直接:既然要知道每一层最右边的节点,那就一层一层遍历,每层结束前把当前层的最后一个节点记录下来。
这里有个关键操作:在遍历当前层之前,先快照当前队列的长度n。因为遍历过程中会不断把下一层节点加入队列,如果不固定n,for循环会把下一层节点也当成当前层处理,最后拿到的“最右节点”就会串层。
用生活类比解释一下:想象排队买奶茶,你只知道当前队伍里有n个人。你只处理这n个人,他们买完后新排进来的人算下一轮。如果不先数清楚人数,你会一直处理到没人排队为止,那就不是“一层一层”而是“一条长队”了。
2.2 完整代码与关键参数说明
from collections import deque class Solution: def rightSideView(self, root: Optional[TreeNode]) -> List[int]: if not root: return [] res = [] q = deque([root]) while q: n = len(q) # 快照当前层节点数 for i in range(n): # 只处理这一层的 n 个节点 node = q.popleft() if node.left: q.append(node.left) if node.right: q.append(node.right) if i == n - 1: # 当前层最后一个节点 res.append(node.val) return res几个细节值得单独说:
if not root: return []:Python里None、空对象都是假值,所以直接判not root最简洁。deque是双端队列,popleft()才能从左端弹出,时间复杂度 O(1)。如果误用pop(),会从右端弹,层序就乱了。i == n - 1的判断写在循环内,意味着每次遍历节点时都要做一次比较。如果你想少做判断,也可以先正常遍历完当前层,然后单独取队列最后一个元素。但我个人喜欢上面的写法,因为逻辑更内聚。
还有一种等价写法:
while q: n = len(q) for i in range(n): node = q.popleft() if node.left: q.append(node.left) if node.right: q.append(node.right) res.append(q[-1].val) # 下一层节点已经在队列尾部这个写法有个陷阱:遍历完当前层后,队列尾部就是下一层最后一个节点,但如果下一层为空,q[-1]会越界。比如输入只有根节点时,遍历完第一层后队列是空的,直接访问q[-1]就报错。所以我还是更推荐在循环内判断i == n - 1。
2.3 复杂度分析与工程适用性
时间复杂度 O(n):每个节点恰好入队、出队一次。
空间复杂度 O(n):最坏情况下,完全二叉树的最后一层有约 n/2 个节点,队列需要存下它们。
从工程角度看,BFS解法是“用空间换层序”。它天然知道当前在第几层,所以一切“按层处理”的题目都应该先想到BFS。后面做二叉树的层序遍历、Z字形成层遍历、找每层最大值,都是这个模板加一点变化。
3. 深度优先(DFS)解法:递归里的“第一次”判断
3.1 核心思想:先右后左 + depth == len(res)
DFS解法的思路比BFS绕一点,但代码更短,而且能帮你深入理解递归。
核心洞察是:如果你先访问右子树、再访问左子树,那么每一层第一次被访问到的节点,恰好就是站在右侧能看到的那一个。
为什么?因为DFS从根节点出发,优先往右走。到达某个深度时,第一个碰到的节点一定是最右边的节点。等这个右子树的分支走完,再回过来访问同一深度左边的节点时,这一层已经有值了,就不再记录。
怎么判断“第一次访问到这个深度”?答案是depth == len(res)。
res列表的下标正好对应层号:res[0]是第0层的右视图节点,res[1]是第1层的,依此类推。当递归进入一个新深度时,这个深度的值还不存在,所以len(res)就等于当前深度。一旦这个深度已经被记录过,len(res)就会大于当前深度。
这个判断是整个DFS解法的灵魂,理解了它,你就理解了“递归深度”和“结果集长度”之间的对偶关系。
3.2 递归代码与栈迭代写法
class Solution: def rightSideView(self, root: Optional[TreeNode]) -> List[int]: res = [] def dfs(node, depth): if not node: return if depth == len(res): res.append(node.val) dfs(node.right, depth + 1) dfs(node.left, depth + 1) dfs(root, 0) return res注意访问顺序:先dfs(node.right),再dfs(node.left)。如果写成先左后右,得到的就是左视图而不是右视图。
有些环境不推荐用递归(比如树特别深,超过了递归栈上限),可以改成显式栈的迭代写法:
class Solution: def rightSideView(self, root: Optional[TreeNode]) -> List[int]: if not root: return [] res = [] stack = [(root, 0)] while stack: node, depth = stack.pop() if not node: continue if depth == len(res): res.append(node.val) # 栈是后进先出,所以先压左子树,再压右子树,弹出时右子树先被访问 stack.append((node.left, depth + 1)) stack.append((node.right, depth + 1)) return res这个迭代版的核心是:入栈顺序和访问顺序相反。想要先访问右子树,就把右子树后压入栈。
3.3 BFS 与 DFS 选型对比
| 维度 | BFS 层序解法 | DFS 深度优先解法 |
|---|---|---|
| 核心思路 | 每层取最后一个节点 | 每层第一个被访问到的节点 |
| 数据结构 | 队列 | 递归栈 / 显式栈 |
| 代码长度 | 约12~15行 | 约8~10行 |
| 时间复杂度 | O(n) | O(n) |
| 空间复杂度 | O(n),队列最多存一层 | O(h),h 是树高,最坏 O(n) |
| 层序理解 | 直观,天然知道层号 | 靠 depth 参数维护深度 |
| 变体适配 | 层序类题目方便 | 与“深度”相关的题目方便 |
(h 为树高:平衡二叉树 h = O(log n),链状树 h = O(n)。所以DFS空间复杂度写作 O(h),最坏 O(n)。)
如果面试只写一种,我会推荐BFS,因为它不容易在递归细节上翻车。但如果你想去大厂面试,最好两种都会,因为面试官很可能会追问“能不能用DFS试试”。这时候你要是只会BFS,就很被动。
4. 写二叉树程序时为什么总报运行时错误:从右视图题延伸的排查清单
很多人在力扣上写二叉树程序,明明思路对了,却总是报AttributeError、RecursionError或者IndexError。这里我结合刷这题的实际经验,把常见运行时错误分成四类,每条都给出排查路径。
4.1 空指针访问:一个if救回来
最常见的报错是:
AttributeError: 'NoneType' object has no attribute 'val'产生原因很直白:你在某个节点为None时,仍然访问了.val、.left、.right。
在右视图这道题里,最容易出现在两个位置:
- 入参
root本身是None,直接执行root.val。 - 递归函数里只判断了
node.left和node.right非空,却忘了当前node可能为空。
我的排查建议是:在递归函数或循环的开头,无条件写上空值判断。不要觉得自己写的树“应该不会空”,测试用例的边界永远比你想的野。
def dfs(node, depth): if not node: return ...4.2 指向关系写成环:无限递归的元凶
第二种报错是:
RecursionError: maximum recursion depth exceeded如果递归里有空值判断,还出现无限递归,十有八九是你把树的指向关系改坏了。
什么叫改坏?比如你在做“二叉树翻转”练习时,先缓存了left,然后node.left = node.right,接着node.right = left,这种操作不会出问题。但如果你在递归里接了父节点的指针,把子节点的left或right指回了父节点,树就变成带环图了。递归永远走不到空节点,最终栈溢出。
排查手段:
- 打印每一层进入递归的节点值,观察有没有重复出现。
- 如果重复出现了某个节点,优先怀疑环引用。
- 检查题目要求“不能修改原树结构”时,你是否在过程中改了
left/right。
4.3 队列、栈和类型声明的经典失误
第三类问题跟数据结构操作有关:
deque是双端队列,出队用popleft()。如果你写pop(),会从右端弹出,层序直接错乱,但不一定会报错,所以更难发现。- 递归里
depth忘了加括号:写成depth + 1传参没问题,但如果你在比较时写if depth == len(res) - 1就会差一层。 - 自定义树节点的类里字段名是
left而不是left_node,抄题时容易写错。 - 用到了
Optional[TreeNode]但没导入:from typing import Optional不能少。不过现在LeetCode环境通常已经帮你包含了,本地调试需要自己注意。
4.4 本地调试方法论:人造小树是底线
刷题时应该在本地把树构造出来,而不是每次只靠提交。右视图这题我建议用一棵比较刁钻的树来调试:
1 / \ 2 3 \ \ 5 4站在右侧看,应该依次看到1 -> 3 -> 4。注意第1层最右边是3,第2层最右边是4(因为3没有左孩子,4是右孩子;如果5在2的右子树,5在第2层深度,但4仍在第2层最右)。
Python本地构造树的代码:
class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right root = TreeNode(1, TreeNode(2, None, TreeNode(5)), TreeNode(3, None, TreeNode(4))) solution = Solution() print(solution.rightSideView(root)) # 期望 [1, 3, 4]如果本地能跑出正确结果,再提交到力扣,运行时错误基本就只可能是边界条件了。
建议在每次while或递归入口加一句临时print,比如输出当前节点的值和深度。树的问题不像数组那样直观,“打印+纸面走查”是性价比最高的调试方式。
5. 这道题的变体与真实场景映射
5.1 左视图、垂直视图、层序变体:一套框架走天下
右视图刷完之后,一定要顺手把几个变体做了,这样框架才能形成体系:
- 左视图:BFS改成取每层第一个节点;DFS改成先递归左子树、再递归右子树,仍然用
depth == len(res)判断。 - 二叉树的层序遍历:BFS里把每层所有节点作为一个列表存进结果。
- 二叉树的锯齿形层序遍历:BFS层序基础上增加一个“方向标志位”,偶数层正序、奇数层反序。
- 二叉树的最大深度:DFS里维护
depth的最大值,或BFS里统计层数。 - 二叉树的最小深度:BFS层序时遇到第一个叶子节点,直接返回当前层数。
这些题的核心区别只在于“记录时机”和“访问顺序”,代码骨架几乎一样。
5.2 目录树、组织架构里的可见范围问题
有人会问:算法题里抽象出来的“右视图”,在真实业务中有什么用?
其实“站在某个视角下,哪些节点可见”是树结构场景里非常常见的需求。
举几个真实例子:
- 权限树:一个公司组织架构是树,某个人能看到的部门边界,可能等价于“从某个根节点出发,按某种遍历顺序取每层第一个满足条件的节点”。
- 目录渲染:文件系统里构造目录树时,右侧栏经常只需要展示每层的一个代表性目录,这就是右视图的逻辑。
- 前端菜单:侧边栏菜单在折叠时只会展示每层第一个可见项,本质上也是一种“视图裁剪”。
甚至在商超货架上,商品的分类层级也是一棵树:先看大类、再看中类、最后看具体品类。“站在顾客角度能看到哪些层级节点”这件事,就是在做树的视图筛选。
你要是能把这层“树的遍历 = 组织信息的可见性控制”的对应关系建立起来,刷题就不再是死记硬背,而是真正在练数据结构思维。
5.3 延伸学习路径:从右视图到线索二叉树
刷完右视图,你已经会了BFS层序和DFS递归。下一步建议按这个路径延伸:
- 二叉树的深度:这题在hot100里也出现过,核心是递归返回值或BFS层数统计。
- 二叉树的前序、中序、后序遍历:尤其建议用迭代栈各写一遍,理解显式栈模拟递归的过程。
- 线索二叉树:如果对“遍历过程中记录前驱后继”感兴趣,可以看看Morris遍历,它可以做到 O(1) 空间完成中序遍历。理解了链式指针的复用,再回看右视图的栈迭代版,就会觉得“原来树的操作本质上是控制指针访问顺序”。
- 二叉搜索树:树的遍历顺序一旦确定,搜索树的有序性就能直接发挥作用。比如判断一棵树是不是BST,用的就是中序遍历的单调性。
二叉树的应用范围远超“刷题”本身——编译器里的抽象语法树、数据库索引的B+树、路由表的Trie树,全都是树。hot100里的树型题目只是给你打地基,右视图则是要求你同时掌握“按层看”和“按深度看”两套地图。
最后说一个我自己的体会:这题我在面试里被问到过两次,一次是校招要求BFS,一次是社招追问能不能DFS。两次都很庆幸自己当时没有只记模板。后来带新人也发现,能把depth == len(res)这个条件讲明白的,二叉树递归基本就过关了一半。如果你刷完这题,能不看题解独立写出两种解法,并且把复杂度分析说清楚,hot100二叉树部分你就已经站在一个很稳的起点上了。