尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

二叉树右视图详解:从层序遍历到BFS与DFS的算法面试实战

二叉树右视图详解:从层序遍历到BFS与DFS的算法面试实战 第一次做这道题的时候我盯着“右视图”三个字看了半天脑子里想的全是“从右边看树”到底是什么样的视角。等到真正理解了题意才发现力扣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 必须注意的两个坑第一个坑是空树的处理。root为null时直接返回空数组。这一步看起来多余但很多人一紧张就忘了结果跑测试用例的时候直接报空指针。第二个坑是层序输入的还原。力扣的测试用例用数组表示二叉树比如[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 0DFS 版本改成先左后右。但如果只回答“改一个条件”显得理解停留在表面最好加上一句左视图和右视图的本质是“每层第一个还是最后一个节点”这取决于遍历顺序和层内判断条件的组合。追问三如果改成从右往左的之字形层序遍历还能用这套模板吗这个时候可以先顺着 BFS 思路说层序遍历保证层级顺序之字形只是把偶数层的顺序反转可以用一个布尔变量控制。然后再补充一句右视图本身和之字形没有直接关系但都是层序遍历模板的变体。这些追问其实在考察一个东西你到底是背下了这道题的代码还是真的理解了层序遍历。把模板吃透这些追问就都不是问题。5. 从右视图延伸到一类题左视图、之字形与N叉树5.1 左视图的最小改动左视图的代码改动小到可以忽略。BFS 版本把记录节点值的条件从i size - 1改成i 0DFS 版本把递归顺序从先右后左改成先左后右。同样是上面的树输出就变成 [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.left和node.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, val0, leftNone, rightNone): 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 二叉树中的最大路径和这类进阶题就不会觉得吃力。最后分享一个我实际面试时的技巧拿到右视图这道题先别急着写代码先当着面试官的面说一句“右视图等价于层序遍历中每层最后一个节点”。这一句话就把题目翻译透了面试官也知道你是真的理解而没背答案这个印象分比任何代码技巧都值钱。我靠这个习惯拿到了好几个面试的后续轮次你可以试试。
返回列表