“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逐行说一下思路边界判断根节点为空直接返回空列表。初始化队列把根节点放进去。外层while q表示还有节点没处理。level_size len(q)固定当前层的节点数量。内层for i in range(level_size)只处理当前层的节点同时把它们的左右孩子追加到队列尾部留给下一轮循环。当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。这不是思路错了而是工具限制。解决办法有两个把递归改成显式栈的迭代写法就像前面 DFS 的迭代版本。用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有时候你在力扣上写得好好的代码搬到自己编辑器里跑不通就是字段名或构造函数对不上。这个坑虽小但报起错来真的很让人摸不着头脑。希望这篇文章能帮你彻底讲透二叉树的右视图也顺手解决你关于“二叉树总报运行时错误”的困惑。有收获的话可以再去找几道变体题练练手我相信等你把左右视图和层序模板都吃透下次再看到任何“按层处理”的树题心里都会很稳。