☰
二叉树右视图详解:BFS与DFS两种解法及错误排查
2026/10/10 11:35:19 网站建设 项目流程

“hot100”这个词,只要是刷过 LeetCode 的人基本都绕不开。而第 199 题“二叉树的右视图”,我愿称它是二叉树入门阶段最值得反复做的一道题。它不偏不怪,既不考什么花哨技巧,也不是单纯的模板背诵,而是真正把“树的形态理解”和“两种遍历思路”串在了一起。很多人在这一题卡住,往往不是不会写遍历,而是没搞明白“右视图”这三个字到底在问什么。这篇就打算把这道题彻底拆开,从模型抽象、BFS 和 DFS 两种实现,到平时写二叉树“总报运行时错误”的排查套路,一次性讲透。

不管你是刚开始刷 hot100 的初学者,还是准备面试想快速梳理二叉树核心题型的选手,这篇文章都值得花几分钟看完。我会尽量用“说人话”的方式把思路讲清楚,代码也给全,你照着敲一遍,再回头看这道题,应该会有完全不一样的感觉。

1. 右视图到底在问什么:先别急着写代码

1.1 题目模型的本质:每一层最右侧的节点

原题描述很直观:给你一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回你能看到的节点值。这个“站在右侧”的描述,第一次看容易让人误解成“沿着右子树一路往下走”。但实际上,你需要返回的不是一条“最右路径”,而是每一层的最右侧节点。

换句话说,右视图 = 把二叉树按层切开,取每一层最右边的那个节点值。题目真正的考点就是能否把“站在右侧看到的节点”抽象成“每一层的最后一个节点”。一旦建立了这个模型,后面 BFS 和 DFS 两种解法其实都是围绕这个模型展开的。

我见过不少人在面试里栽在这一题,原因就是写代码前没把模型理清楚,上来就递归找最右路径,最后返回一个“右链”而不是“右视图”。所以请先记住这个核心结论:右视图不是“一路向右”,而是“每层取末”。

1.2 反直觉的经典例子:为什么深层左子树也会出现在右视图里

很多人第一次写错,是因为没想通“左侧的深层节点凭什么能被看到”。我们来看一个非常经典的反例:

1 / \ 2 3 \ 4

这棵树如果按“一路向右”的想法,右视图应该是[1, 3],因为 2 在左边,4 在更深的地方。但正确答案是[1, 3, 4]。

仔细想想:站在树的右侧往左看,第三层只有一个节点 4,右边没有任何节点挡住它,所以你当然看得到。这就解释了为什么左子树的深层节点可能出现在右视图里——右视图关注的是“某一层最靠右的节点”,而不是“某一棵右子树里的节点”。

边界条件也要留意:

  • 空树:返回[]。
  • 只有一个根节点:返回[根节点值]。
  • 左子树很高、右子树很矮:右视图会包含左侧深处的节点,但右侧节点的优先级始终高于左侧同层节点。

把这几个例子在纸上画一遍,比你盲写十遍代码都有用。模型对了,代码只是水到渠成的事。

2. 解法一:层序遍历(BFS),最直观也最稳

2.1 为什么 BFS 天然适合这道题

既然右视图的本质是“获取每一层最右侧的节点”,那最容易想到的思路自然是:把整棵树一层一层扫出来,然后每层取最后一个节点。这正是广度优先遍历(BFS)擅长的事情。

BFS 的过程特别像“按楼层看一栋楼”:先看第一层有哪些房间,再看第二层有哪些房间,每看完一层,就把这一层最右边那个房间记下来。用队列实现时,我们不需要真的把整层单独存出来,只要在做 while 循环之前记录当前队列的长度,这个长度就是“当前层的节点数”,然后只循环这么多次,队列里剩下的就都是下一层的节点。

这里有个细节值得强调:很多人写层序遍历时会犯一个错——在循环里不断len(q),结果队列长度一直在变,导致一层没处理完就混入了下一层的节点。正确的做法是进入每一层之前,先用一个变量level_size = len(q)固定当前层的节点数。

2.2 Python 代码实现与逐步拆解

用 Python 写的话,标准库里的collections.deque是首选,因为popleft()是 O(1) 时间;如果用list的pop(0),每次都要移动整个数组,数据量一大就会拖慢速度。完整代码如下:

from collections import deque class Solution: def rightSideView(self, root: Optional[TreeNode]) -> List[int]: if not root: return [] res = [] q = deque([root]) while q: level_size = len(q) for i in range(level_size): node = q.popleft() # 当前层最后一个节点,就是右视图能看到的节点 if i == level_size - 1: res.append(node.val) if node.left: q.append(node.left) if node.right: q.append(node.right) return res

逐行说一下思路:

  1. 边界判断:根节点为空,直接返回空列表。
  2. 初始化队列,把根节点放进去。
  3. 外层while q表示还有节点没处理。
  4. level_size = len(q)固定当前层的节点数量。
  5. 内层for i in range(level_size)只处理当前层的节点,同时把它们的左右孩子追加到队列尾部,留给下一轮循环。
  6. 当i == level_size - 1,说明当前节点是这一层的最右端节点,把它的值加入结果。

如果你觉得“判断索引是不是最后一个”不够直观,也可以先把当前整层节点收集到一个tmp列表里,等循环结束后取tmp[-1]。两种写法我都试过,直接判断索引更省空间,代码也更紧凑;用tmp列表可读性更好,更适合在面试时边写边讲思路。看个人习惯,但核心都是“按层处理”。

2.3 复杂度分析与耗时实测

时间复杂度:每个节点只会入队一次、出队一次,BFS 整体的复杂度就是 O(n),其中 n 是二叉树节点总数。

空间复杂度:最坏情况下,队列中最多会同时存在一整层的节点。在完全二叉树中,最底层的节点数约为 n/2,所以空间复杂度是 O(n)。日常写题时不需要过度纠结这个上限,记住“层序遍历要用队列,空间大约是某一层的宽度”就够了。

实际跑 LeetCode 时,Python 版本的耗时通常在 20~30ms 左右,击败比例看当期提交情况。这个性能在面试中完全够用,不需要再做微优化。

3. 解法二:深度优先遍历(DFS),先右后左的巧妙思路

3.1 为什么 DFS 也能求右视图

如果你以为 BFS 是唯一解,那就错过了一个非常漂亮的思路。深度优先遍历(DFS)也可以求右视图,关键点在于访问顺序:先递归右子树,再递归左子树。

因为右视图要的是“每一层最右侧的节点”,如果我们每层都从右边开始访问,那么每一层第一个被访问到的节点,一定就是该层最右侧的节点。这个思路的实现方式,是让递归函数携带一个depth参数,然后判断“当前深度是否已经出现过节点”:

  • 如果depth == 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

这段代码非常短,但每个if都有含义。递归的终止条件写在最前面:if not node: return,这样在函数开头统一处理空节点,后面就再也不用担心空指针的问题。if depth == len(res)这一步是核心,它利用了“结果数组长度等于当前已探索的深度层数”这个性质来做去重,非常巧妙。

不过要注意,递归实现有一个隐患:如果二叉树特别深(比如退化成一条链),Python 的递归深度可能超过默认限制,抛RecursionError: maximum recursion depth exceeded。虽然 LeetCode 的一般测试用例很少出现这种极端情况,但在本地自测或者面试现场,最好心里有数。这时可以考虑用栈模拟 DFS 的迭代版本:

class Solution: def rightSideView(self, root: Optional[TreeNode]) -> List[int]: res = [] if not root: return res stack = [(root, 0)] while stack: node, depth = stack.pop() if depth == len(res): res.append(node.val) # 注意压栈顺序:先压左再压右,弹栈时才会先处理右 if node.left: stack.append((node.left, depth + 1)) if node.right: stack.append((node.right, depth + 1)) return res

这段迭代版的压栈顺序我踩过坑,特意强调一下:栈是后进先出,所以你想让右子树先被处理,就要让右子树后进栈,也就是代码里先压node.left,再压node.right。如果顺序写反,那就变成“先左后右”的普通前序遍历,结果就不对了。

3.3 BFS 和 DFS 两种解法怎么选

很多初学者会觉得“既然 BFS 这么直观,为什么还要学 DFS 的做法?”我的看法是:这道题两种解法各有优势,面试时如果能流畅地写出两种,是非常加分的。下面这个表格可以帮你快速做决策:

对比维度BFS 层序遍历DFS 先右后左
核心思想按层扫描,取每层最后一个节点每层优先访问最右节点,首次遇到即记录
数据结构队列(deque)递归栈(或显式栈)
实现难度易理解,代码较长代码简洁,思路需要转折
时间复杂度O(n)O(n)
空间复杂度O(树的宽度)O(树的高度)
适用场景需要每层完整信息时更顺手只关心每层第一个可见节点时更高效

如果题目后续要改成“返回每层的最左侧节点”或者“之字形打印”,BFS 的模板复用率会更高;如果面试官只要求一个简洁解法,DFS 往往两三行核心逻辑就能写完。

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

热词榜里有个问题非常真实:写二叉树程序时为什么总是报运行时错误。我自己初学阶段也经历过,明明思路看起来没问题,一提交就红一片。下面把这些年踩过、帮别人排查过的坑集中总结一下,希望能让你少走弯路。

4.1 空指针访问:绝大多数运行时错误的根源

二叉树运行时错误里,最常见的报错长这样:

AttributeError: 'NoneType' object has no attribute 'left'

出现这个错误,十有八九是你忘了判断节点本身是否为None。比如你直接写root.left.val,但root可能是空节点,程序就会炸。

二叉树的天然结构决定了它到处都是“空”:叶子节点的左右孩子是空,某个节点可能只有一个孩子,根节点可能本身就是空。所以在写任何对节点的字段(val、left、right)访问之前,都要问自己一个问题:这个节点会不会是None?

正确的做法是:递归函数在最前面统一处理空节点,或者像前面 BFS 代码那样,在把子节点加入队列之前先判断它是否存在。这一点说起来简单,但一紧张就容易漏。

4.2 递归退出条件写得不完整

我见过不少初学者写递归时,会把终止条件写得很“花”,比如:

def dfs(node): if not node.left and not node.right: # 处理叶子节点 return # 递归...

这种写法在叶子节点上确实能退出,但问题在于:如果node本身就是None,那node.left直接就会报错。更稳妥的写法是把空节点判断放在最前面:

def dfs(node): if not node: return # 处理当前节点,再递归左右孩子

等你习惯了这个写法,几乎所有二叉树递归题都能用同一套骨架套上去。千万不要为了省一行判断而省略空节点处理。

4.3 递归深度导致栈溢出

递归写法代码简洁,但 Python 默认递归深度限制一般是 1000 层。如果二叉树是极端链状结构(比如每个节点只有右孩子),递归就会一直往下钻,最终抛RecursionError。

这不是思路错了,而是工具限制。解决办法有两个:

  1. 把递归改成显式栈的迭代写法,就像前面 DFS 的迭代版本。
  2. 用sys.setrecursionlimit()临时调大限制,但这是治标不治本,而且深度太大仍然可能导致程序崩溃。

实际刷题时,LeetCode 的一般用例很少把树建到 1000 层,但如果是自己本地测试极端数据,就要注意这个问题。

4.4 调试二叉树的实用套路

报错之后光盯着代码看,效率很低。我自己的排查套路是这样的:

  • 先用最小用例自测:[]、[1]、[1, 2]、[1, 2, 3],这四种简单的输入能快速暴露空指针和边界问题。
  • 再找一个“歪树”用例:比如[1, 2, null, 3, null, 4],这类树容易暴露访问顺序的问题。
  • 在关键位置打印节点值和深度:比如在前面 DFS 实现里,临时加一行print(node.val, depth),能看到程序的访问顺序是否符合“先右后左”。
  • 用一个能可视化打印二叉树的工具函数,把输入和输出对照着看,比自己脑补树的形状快得多。

下面这个表格可以直接收藏,以后再遇到二叉树报错,按图索骥:

报错类型常见原因修复策略
AttributeError: 'NoneType' object has no attribute 'left'访问了空节点的属性访问前加if node:或递归开头统一判空
RecursionError: maximum recursion depth exceeded递归深度过大或递归终止条件错误改迭代写法;检查终止条件;必要时调大递归限制
IndexError: list index out of range对结果数组按下标取值时越界检查depth == len(res)这类边界判断是否写对
结果长度不对 / 顺序不对遍历顺序错了(比如本该先右后左写成了先左后右)画图模拟一遍访问顺序,打印日志比对

4.5 一个小技巧:先画一棵“丑树”再写代码

我强烈建议你在动手写二叉树代码之前,先随手画一棵不对称的树,比如根节点左边很深、右边只有一个节点。然后在这棵树上标好每个节点从上到下的层序号,再模拟一遍代码的执行过程。

这个方法对右视图这道题尤其有效,因为按照我的经验,它能把“每层最右”这个模型直观地刻进脑子里。等你写完之后,用同样一棵树去验证输出,如果结果和你肉眼判断的“可见节点”一致,那代码基本就稳了。

5. 从右视图出发,能延伸出哪些同类题

5.1 左视图:一套思路的对称变换

学会了右视图,左视图几乎是白送的。只要把思路反过来:

  • BFS 版本:每层取第一个节点,而不是最后一个。
  • DFS 版本:先递归左子树,再递归右子树,“每一层第一次被访问到的节点”就是最左节点。

具体代码就是把右视图里的node.right和node.left对调,以及层内索引判断从level_size - 1改成0。

5.2 之字形层序遍历:层序模板的升级版

LeetCode 第 103 题“二叉树的锯齿形层序遍历”就是在层序遍历的基础上,增加一个按层翻转的标志。比如从第二层开始,偶数层从右往左输出,奇数层从左往右输出。

如果你把右视图的 BFS 模板练熟了,做这道题会非常顺手。同样是固定level_size,只不过在把当前层收集完之后,判断一下层号奇偶决定是否reverse。

5.3 把“视图”思维迁移到工程场景

可能有读者会问:“这种题除了面试还有什么用?”其实“按层取端点”的思维在业务里很常见。比如组织架构树要生成某个层级的默认展示名单、目录树渲染时需要高亮每一层最末节点、权限树的层级归并,这些场景本质上都是对树做按层处理。

理解了右视图的模型后,你在写业务代码时看到“树形结构”就不会条件反射地只想着递归,而是会思考“我到底要的是全量节点、某一层节点,还是每一层某个特定位置的节点”。这种抽象能力的提升,才是刷 hot100 真正的价值所在。

5.4 一个适合继续挑战的进阶方向:输出树的“轮廓”

右视图再往前一步,有一个更有意思的题:输出二叉树的轮廓,也就是从左视图和右视图合并之后,去掉遮挡关系,得到一个从根节点延展到叶子节点的“外圈节点”序列。

这个题不要求你现在就做,但你可以用它检验自己对树的遍历理解程度。当你把右视图、左视图、叶子节点三条遍历串在一起的时候,很多关于树结构的直觉会自然建立起来。我个人刷题后期的体会是:同类题做多了之后,拼的不是谁背的代码多,而是谁脑子里树的画面更清楚。

最后再分享一点我的个人习惯

这道题我每次带人过 hot100 都会讲,而且强调“先用 BFS 拿分,再用 DFS 炫技”。面试时,如果你能先把 BFS 版写到无懈可击,再补一个 DFS 的简洁写法,面试官基本不会再在这道题上追问。

我自己刷过几轮之后,最大的体会是:右视图这类题真正考的,是你把一个场景描述转换成数据模型的能力。“站在右侧看”听起来像空间想象,但实际上就是“每层取最后一个节点”。一旦你习惯了用这种抽象方式去理解题目,很多二叉树的题目都会变得非常亲切。

对了,还有一个小细节:如果你的本地环境需要自己定义TreeNode,记得先确认节点类的字段名是left和right,有时候你在力扣上写得好好的代码,搬到自己编辑器里跑不通,就是字段名或构造函数对不上。这个坑虽小,但报起错来真的很让人摸不着头脑。

希望这篇文章能帮你彻底讲透二叉树的右视图,也顺手解决你关于“二叉树总报运行时错误”的困惑。有收获的话,可以再去找几道变体题练练手,我相信等你把左右视图和层序模板都吃透,下次再看到任何“按层处理”的树题,心里都会很稳。

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

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

立即咨询