二叉树右视图详解:从层序遍历到BFS与DFS的算法面试实战
2026/9/16 4:30:36 网站建设 项目流程

第一次做这道题的时候,我盯着“右视图”三个字看了半天,脑子里想的全是“从右边看树”到底是什么样的视角。等到真正理解了题意,才发现力扣hot100里的这道199题,本质上是把“视角”翻译成“数据结构语言”的过程。二叉树的右视图,剥掉题目的包装,就是层序遍历的每一层取最后一个节点。就这么一句话,能让你在面试里少走很多弯路。

这篇文章我打算从新手的视角出发,把BFS按层取尾和DFS深度记录法两种解法都拆开揉碎,再把面试时容易被追问的点、边界条件的坑、以及和hot100里其他二叉树题目的联动关系一起捋清楚。不管是刚刷题的小白,还是准备跳槽的开发者,按这个思路走一遍,这道题基本就彻底拿下了。

1. 右视图到底在考什么:从“视角”到“层序”的翻译

1.1 题目描述与直觉理解

题目给一棵二叉树,要求站在树的右侧往左看,返回能看到的节点值,从上到下排序。举个例子:

1 / \ 2 3 \ \ 5 4

站在右侧看,能看到的是 1、3、4 这三个节点,输出 [1, 3, 4]。这里有个特别反直觉的点:节点 5 虽然在右边,但它被 4 挡住了,所以看不到。换句话说,右视图不是“右子树上的节点”,而是“每一层最右边的节点”。

我第一次做的时候,第一反应是递归遍历右子树,一路往右走。结果遇到上面的例子就翻车了——如果右子树为空,左边更深层的节点一样能被看到。比如:

1 / 2 / 3

这棵树根本没有右子树,但从右侧看,1、2、3 全都能看到,输出是 [1, 2, 3]。所以“一直往右递归”这条路是走不通的,必须回到层级的视角来思考。

1.2 核心考点拆解

这道题在力扣上的难度是中等,但它的核心考点一点也不复杂:二叉树的层序遍历。把层序遍历写熟了,右视图、左视图、之字形遍历、自底向上遍历这些变体,本质上都是同一套模板在改条件。

具体来说,题目考察三个层次的能力:

  • 能不能把生活化的“视角”抽象成数据结构操作。站在右侧看 = 每层最右节点,这个抽象一旦完成,代码就是一个模板替换。
  • 层序遍历的扎实程度。队列 + 分层处理,是不是能一边写一边解释清楚为什么要在循环前固定 size。
  • 对递归遍历顺序的理解。DFS 解法要求你先访问右子树再访问左子树,并且通过深度和结果数组长度的关系来判断是否记录,这比单纯背模板要求更高。

前两个层次对应 BFS 解法,第三个对应 DFS 解法。下面分别展开。

2. BFS按层取尾:队列解法是最稳妥的突破口

2.1 队列 + size 分层的完整思路

BFS 的思路非常直观:用队列维护当前层的节点,每次处理一整层,把这一层最后一个节点的值加入结果数组。关键点在“每次处理一整层”这句话上——怎么保证队列里恰好是同一层的节点?

答案是:在进入内层循环之前,先把当前队列的长度存下来,这个长度就是当前层的节点数。然后只弹出这么多个节点,每弹出一个,就把它的左右子节点加到队尾。这些子节点属于下一层,留到下一轮循环再处理。

1 / \ 2 3 第一轮:队列 = [1],size = 1,弹出 1,记录 1,加入 2 和 3 第二轮:队列 = [2, 3],size = 2,弹出 2 和 3,记录 3,加入 2 和 3 的子节点

如果不在循环前固定 size,而是在循环里动态获取队列长度,那就乱了——因为循环过程中队列不断有新节点入队,长度一直在变,层的边界就丢了。这是层序遍历里最容易翻车的点,没有之一。

2.2 Python 与 JavaScript 实现

我用 Python 写的 BFS 版本是这样的:

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

关键就两行:size = len(queue)固定当前层节点数;if i == size - 1判断是不是本层最后一个节点。这个判断放在for循环里面,每个节点都走一遍,遇到最后一个就记录。

如果面试的时候用的是 JavaScript,逻辑一模一样:

var rightSideView = function(root) { if (!root) return []; const result = []; const queue = [root]; while (queue.length > 0) { const size = queue.length; for (let i = 0; i < size; i++) { const node = queue.shift(); if (i === size - 1) result.push(node.val); if (node.left) queue.push(node.left); if (node.right) queue.push(node.right); } } return result; };

JavaScript 里用数组模拟队列,shift()出队、push()入队。唯一的缺点是shift()是 O(n) 的复杂度,严格说不如 Python 的deque高效,但力扣数据规模下完全够用。

2.3 BFS 必须注意的两个坑

第一个坑是空树的处理。rootnull时,直接返回空数组。这一步看起来多余,但很多人一紧张就忘了,结果跑测试用例的时候直接报空指针。

第二个坑是层序输入的还原。力扣的测试用例用数组表示二叉树,比如[1,2,3,null,null,4,5]表示一棵树。这个数组本身不是层序遍历的输出,而是带null占位的层序描述。我调试的时候经常想在本地跑代码,就需要一个把数组还原成树结构的辅助函数,这个在后面的边界条件部分会一起给出。

BFS 解法的时间和空间复杂度都是 O(n),n 是节点总数。空间上最坏情况是树完全平衡时,最后一层有约 n/2 个节点,队列同时占用这么多空间。实际刷题时这个空间开销完全可接受,但如果面试官追问“能不能降低空间复杂度”,就到了 DFS 解法登场的时候。

3. DFS深度记录法:递归版右视图的底层逻辑

3.1 根右左遍历顺序与 depth 的配合

BFS 是“一层一层扫描”,DFS 则是“一条路走到底再换一条”。右视图的 DFS 解法比 BFS 更巧妙,代码也更短,但理解门槛稍高一些。

核心思路:如果每次优先访问右子树,那么在每一层,第一个被访问到的节点一定就是从右边能看到的节点。

1 / \ 2 3 \ \ 5 4

递归顺序是:1 → 3 → 4 → 2 → 5。站在右侧看,第 0 层第一个访问到的是 1,第 1 层第一个访问到的是 3,第 2 层第一个访问到的是 4。正好对应 [1, 3, 4]。

怎么判断“第一个访问到”?用一个结果数组,数组的下标对应层数。当递归深度depth正好等于len(result)时,说明这一层还没有记录过任何节点,那么当前节点就是这一层第一个被访问到的节点,加入结果。之后这一层再访问到其他节点,depth仍然等于len(result)?不对,因为结果数组已经加了一个元素,len(result)变大,条件不再成立,所以不会被重复记录。

这套逻辑是这道题的精髓:用结果数组的长度当作“已覆盖的最大层数”的判断依据,既不需要哈希表,也不需要额外的标记数组。

3.2 代码实现

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

核心代码就这么几行。注意递归顺序是先右后左,顺序反了就变成左视图了。我在这个地方栽过跟头,因为二叉树的遍历默认都是先左后右,潜意识里很容易写成dfs(node.left)在前。

3.3 递归与迭代的取舍

DFS 解法看起来优雅,但它是递归实现的,面试时要能回答两个追问:

  • 递归深度会不会溢出?如果树退化成一条链,深度为 n,递归调用栈会占用 O(n) 空间。Python 默认递归深度限制是 1000,力扣的数据规模一般不会触发,但面试时可以提一句“递归实现依赖系统栈,极端情况下可能栈溢出,但工程上可以用显式栈改写”。
  • 能不能用显式栈模拟?可以,但代码量会明显增加。需要同时维护节点和它对应的深度,而且栈是后进先出,要先压左子树再压右子树,出栈顺序才是“右先左后”。这个写起来比较绕,实际面试中我不会首选。

我的个人经验是:如果面试官没有特殊要求,优先写 BFS——它更直观,不容易出错,而且层序遍历本身就是二叉树的基础考点。如果想展示对递归理解更深,DFS 是很好的加分项,前提是你能把depth == len(result)这个判断讲清楚。如果讲不清楚,面试官反而会怀疑你在背题。

4. 两种解法对比与面试选型参考

4.1 复杂度与代码量对比

把两种解法放在一起看,差异很清晰:

对比维度BFS 队列解法DFS 递归解法
时间复杂度O(n)O(n)
空间复杂度O(n),最坏情况队列存一整层O(h),h 为树高,最坏 O(n)
代码量约 15 行约 10 行
思路难度低,层序遍历模板中,需要理解先右后左和 depth 判断
适用树形任意任意,但深树需注意递归栈
面试首推推荐优先写可作为加分项

空间复杂度是两者最值得说道的区别。BFS 在最坏情况下(完全二叉树)队列里同时存在约 n/2 个节点;DFS 在平均情况下只占 O(log n) 的递归栈,但最坏情况(链表状树)也是 O(n)。所以“DFS 一定比 BFS 省空间”是个误区,只能说在平衡树上 DFS 更省。

4.2 面试中如何应对追问

面试官看到你写完代码,大概率会从下面几个角度追问:

追问一:如果这棵树特别深,你的解法会有问题吗?

这个问题考验的就是空间复杂度意识。如果你写的是 DFS 递归版本,要承认递归深度可能成为问题,然后补充可以用迭代栈或者转 BFS。如果你写的是 BFS,可以回答“队列空间和树宽成正比,树很宽时空间开销大,但不会栈溢出”,然后顺势提一下 DFS 在有高树优势。

追问二:如果改成左视图,你改哪里?

这是送分题变体。BFS 版本改成if i == 0,DFS 版本改成先左后右。但如果只回答“改一个条件”,显得理解停留在表面,最好加上一句:左视图和右视图的本质是“每层第一个还是最后一个节点”,这取决于遍历顺序和层内判断条件的组合。

追问三:如果改成从右往左的之字形层序遍历,还能用这套模板吗?

这个时候可以先顺着 BFS 思路说:层序遍历保证层级顺序,之字形只是把偶数层的顺序反转,可以用一个布尔变量控制。然后再补充一句,右视图本身和之字形没有直接关系,但都是层序遍历模板的变体。

这些追问其实在考察一个东西:你到底是背下了这道题的代码,还是真的理解了层序遍历。把模板吃透,这些追问就都不是问题。

5. 从右视图延伸到一类题:左视图、之字形与N叉树

5.1 左视图的最小改动

左视图的代码改动小到可以忽略。BFS 版本,把记录节点值的条件从i == size - 1改成i == 0;DFS 版本,把递归顺序从先右后左改成先左后右。同样是上面的树,输出就变成 [1, 2, 5]。

这个改动是理解右视图的试金石。如果你能脱口而出这两个改动点,说明你已经理解了层序和遍历顺序对“可见性”的影响,而不是只记住了答案。

5.2 之字形遍历与右视图的联动

之字形遍历是力扣 103 题,要求第一层从左往右、第二层从右往左、第三层从左往右,交替输出。它和右视图配合起来看,正好能把层序遍历的三种变体一次练透。

之字形的 BFS 解法在层序遍历模板的基础上加一个方向标志:

def zigzagLevelOrder(root): if not root: return [] result = [] queue = deque([root]) left_to_right = True while queue: size = len(queue) level = [] 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) if not left_to_right: level.reverse() result.append(level) left_to_right = not left_to_right return result

右视图其实可以看作“之字形遍历的每层最后一个节点”的一个特例——但不要真的这么理解,因为右视图不需要维护整个 level 列表,直接在层内判断最后一个即可。把这两道题连着刷,层序遍历这套模板就滚瓜烂熟了。

5.3 如果树不是二叉树,而是N叉树,怎么做右视图?

N叉树的右视图是力扣上下架过的一道题,思路完全一致。BFS 模板几乎不用改,只是把node.leftnode.right的入队变成遍历node.children列表:

def rightSideViewNary(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) queue.extend(node.children) return result

注意queue.extend(node.children)这一步是把所有子节点批量入队,顺序无所谓——反正只取每层最后一个。

5.4 现实场景:从右视图到“可见性”问题

这类“右侧可见”的问题,在图形学和游戏开发里其实有个更专业的名字:可见性判断。比如游戏引擎在渲染场景时,需要判断哪些物体从当前相机视角看是可见的、哪些被遮挡了。复杂场景里会用到遮挡剔除、八叉树剖分、深度缓冲等技术,但概念内核和二叉树的右视图是一致的——从一个方向看去,哪些对象处于未被遮挡的状态。

这样类比不是为了让你去学图形学,而是帮助你理解为什么面试官喜欢考这种题:右视图这个抽象概念能延伸到更广阔的领域。算法题的价值从来不在于题本身,而在于它代表的那类思维模型。

6. 刷题复盘:边界条件与常见错误汇总

6.1 边界条件自测清单

每次提交之前,我会习惯性地过一遍这些边界情况。这里整理成了一份清单,刷任何二叉树题目都通用:

测试形态树的结构期望输出
空树[][]
单节点[1][1]
只有左子树[1,2][1,2]
只有右子树[1,null,3][1,3]
左右子树高度不同[1,2,3,null,5,null,4][1,3,4]
退化成链表[1,2,null,3,null][1,2,3]

特别提醒:只有左子树这个用例最容易漏。很多人默认右视图一定要从右子树取节点,实际上左子树深处的节点也可能被看到。

6.2 本地调试:数组转树辅助函数

力扣的运行环境已经帮你把数组转成了树节点,但本地调试时没有这个福利。我自己写了一个简单的辅助函数,用来把力扣的测试用例数组还原成树结构:

class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def build_tree(values): if not values: return None root = TreeNode(values[0]) queue = deque([root]) i = 1 while queue and i < len(values): node = queue.popleft() if i < len(values) and values[i] is not None: node.left = TreeNode(values[i]) queue.append(node.left) i += 1 if i < len(values) and values[i] is not None: node.right = TreeNode(values[i]) queue.append(node.right) i += 1 return root

注意力扣的数组是带null占位的层序描述,比如[1,2,3,null,null,4,5]null表示对应位置没有节点,但占位仍然存在,所以索引 i 必须逐个递增,不能跳过。有了这个函数,就可以在本地跑完整的测试流程,调试效率高很多。

6.3 我踩过的坑与排查思路

第一个坑:把右子树和右视图划等号。第一次提交时我写了一个递归版,只走node.right,结果在左右子树都有数据的用例上直接失败。排查思路是把测试用例画出来,逐层标注可见节点,才意识到问题出在层级的抽象上。

第二个坑:BFS 里忘了固定 size。我早期的层序遍历代码喜欢这样写:

while queue: node = queue.popleft() # 处理节点 queue.append(node.left) queue.append(node.right)

这个写法只能做到“逐节点”遍历,分不清层。在右视图里直接导致结果数组里全是每一层靠右的分散节点,顺序和层级全乱了。排查方法是打印每一轮队列的长度变化,立刻就能看出来层边界丢失了。

第三个坑:DFS 方向搞反。这个前面提到过,根深蒂固的“先左后右”惯性,导致第一次写 DFS 版本时输出的是左视图。排查方式也很简单:拿[1,2,3,null,null,4,5]画一下递归调用顺序,只要画出第一步是去右子树还是左子树,问题就一目了然。

6.4 这道题在 hot100 里的定位与联动刷题建议

力扣 hot100 里的二叉树题有一条很清晰的打怪路线:先做 104 二叉树的最大深度,再到 102 二叉树的层序遍历,然后做 199 二叉树的右视图。这三道题是递进关系,104 让你理解递归深度,102 让你掌握层序遍历模板,199 则逼你把模板变成自己的东西。

做完这三道题,我建议接着刷三道巩固一下:226 翻转二叉树(理解递归的左右交换)、101 对称二叉树(理解遍历顺序对结果的影响)、103 二叉树的之字形遍历(理解层序变体)。这六道题放在一起练,二叉树遍历的基本功就相当扎实了,后面再做 105 从前序与中序遍历构造二叉树、124 二叉树中的最大路径和这类进阶题,就不会觉得吃力。

最后分享一个我实际面试时的技巧:拿到右视图这道题,先别急着写代码,先当着面试官的面说一句“右视图等价于层序遍历中每层最后一个节点”。这一句话就把题目翻译透了,面试官也知道你是真的理解而没背答案,这个印象分比任何代码技巧都值钱。我靠这个习惯拿到了好几个面试的后续轮次,你可以试试。

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

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

立即咨询