☰
二叉树的右视图:从层序到深度优先的两种解法与工程实践
2026/10/9 6:56:34 网站建设 项目流程

力扣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递归。下一步建议按这个路径延伸:

  1. 二叉树的深度:这题在hot100里也出现过,核心是递归返回值或BFS层数统计。
  2. 二叉树的前序、中序、后序遍历:尤其建议用迭代栈各写一遍,理解显式栈模拟递归的过程。
  3. 线索二叉树:如果对“遍历过程中记录前驱后继”感兴趣,可以看看Morris遍历,它可以做到 O(1) 空间完成中序遍历。理解了链式指针的复用,再回看右视图的栈迭代版,就会觉得“原来树的操作本质上是控制指针访问顺序”。
  4. 二叉搜索树:树的遍历顺序一旦确定,搜索树的有序性就能直接发挥作用。比如判断一棵树是不是BST,用的就是中序遍历的单调性。

二叉树的应用范围远超“刷题”本身——编译器里的抽象语法树、数据库索引的B+树、路由表的Trie树,全都是树。hot100里的树型题目只是给你打地基,右视图则是要求你同时掌握“按层看”和“按深度看”两套地图。

最后说一个我自己的体会:这题我在面试里被问到过两次,一次是校招要求BFS,一次是社招追问能不能DFS。两次都很庆幸自己当时没有只记模板。后来带新人也发现,能把depth == len(res)这个条件讲明白的,二叉树递归基本就过关了一半。如果你刷完这题,能不看题解独立写出两种解法,并且把复杂度分析说清楚,hot100二叉树部分你就已经站在一个很稳的起点上了。

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

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

立即咨询