二叉树右视图:BFS与DFS遍历的核心思路与实战坑点

📅 发布时间:2026/10/9 6:56:40
二叉树右视图:BFS与DFS遍历的核心思路与实战坑点
刷算法题的时候最怕遇到那种“看着简单、一写就废”的题目。力扣hot100里的“二叉树的右视图编号40”就是非常典型的一道。第一次看到这个题大多数人会觉得不就是层序遍历取每层最后一个节点嘛结果一写就发现不对劲尤其是递归方案很容易被各种边界条件折磨。这篇文章我把这题的两种主流解法、背后的遍历原理、以及我实际刷题过程中踩过的坑一次说清楚希望能给你省点时间。1. 项目概述与核心思路拆解1.1 题面拆解右视图到底在问什么先看清楚题目的本质。给定一棵二叉树想象自己站在它的右侧按照从顶部到底部的顺序返回从右侧所能看到的节点值。说白了就是求每一层最右边的那个节点。这里有个容易混淆的点最右边的节点不一定是右子节点。如果某一层的右边子树是空的那么左侧子树中靠右的节点就会出现在视野里。举个例子一棵树只有左子树一路向下右子树全是空的那么从右面看过去看到的反而是左子树上每层的节点。所以“右视图”的准确含义是“每一层在水平方向上的最后一个节点”而判断标准是层不是左右孩子。理解了这个思路就清晰了只要能按层遍历二叉树并且每层记录最后一个节点就能得到答案。这提示了两种最自然的方案——广度优先搜索BFS层序遍历和深度优先搜索DFS前序遍历的变体。hot100题单里为什么选这道题因为它同时考察了两种基本遍历的变形应用属于“基础操作一点思维转换”的组合。做过之后你会发现它其实是很多后续题目的地基比如二叉树的锯齿形遍历、最大宽度、填充节点的下一个右侧节点指针都和这题的思路有直接关联。1.2 两种解法选型BFS还是DFS我刷题的习惯是遇到这种可以多解的题目先把所有解法的思路在脑子里过一遍再根据数据规模和题目限制做取舍。这道题两种做法都能过但是它们的思考方式和适用场景完全不同写起来手感也不一样建议都亲手实现一遍。BFS解法最直观维护一个队列每次处理完整的一层把每层最后一个节点的值记下来。这种解法适合那些“按层处理”的场景代码结构很固定逻辑不容易出错。DFS解法则更像是在记录一条“最右路径”但要配合深度信息才能正确判断该深度是否已经有节点被记录过。它适合那些“按深度处理”的场景空间占用在某些极端情况下反而更可控。从面试的角度看两种解法最好都掌握。面试官喜欢看你能不能从不同角度切入同一个问题并且能讲清楚各自的时间复杂度和空间复杂度。BFS的空间复杂度在最坏情况下是O(n)因为队列里可能装一整层的节点DFS在最坏情况下递归深度是树的高度空间复杂度是O(h)h是树高。对于右视图这个场景如果给你的是一棵很深的退化树DFS的递归调用栈可能更深但实际做题时两者都不会有性能问题力扣的测试数据规模并不大。2. 核心细节解析与数据结构选型2.1 BFS层序解法的关键参数先看BFS写法。这里最核心的变量是queueTreeNode* q和每一层的节点数量size。为什么会反复强调size要先取出来因为队列在遍历过程中会不断入队新节点如果循环条件直接写成while (!q.empty())你根本分不清当前处理的节点到底是这一层的还是下一层的。必须先记录int size q.size()然后用for (int i 0; i size; i)去消费这个固定数量的节点这样每一层就能被精确切分。代码模板大概是这样的/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */ class Solution { public: vectorint rightSideView(TreeNode* root) { vectorint res; if (!root) return res; queueTreeNode* q; q.push(root); while (!q.empty()) { int size q.size(); // 当前层节点数 for (int i 0; i size; i) { TreeNode* node q.front(); q.pop(); if (i size - 1) { // 当前层的最后一个节点 res.push_back(node-val); } if (node-left) q.push(node-left); if (node-right) q.push(node-right); } } return res; } };这里要注意的是判断“最后一个节点”的位置。你可以在遍历完一层之后再单独把最后一个节点加入结果也可以在遍历过程中判断i size - 1时记录下来。我更喜欢后者少一次记录步骤逻辑也更紧凑。有个小坑如果你写的是if (i size)那就永远等不到这个条件成立因为i最大也就是size - 1。初学者在这里报“输出为空”或者“缺最后一个元素”的时候先检查这里。2.2 DFS解法的递归设计与先后顺序DFS解法是这题的进阶亮点。核心思路是我们用一个变量depth记录当前访问的深度同时用一个结果数组res。根据右视图的定义当某个深度第一次被访问时第一个访问到的节点一定是从右侧能看到的最右节点。但前提是必须先访问右子树再访问左子树。递归函数的参数一般设计成三个当前节点root、当前深度depth、结果引用res。只要满足depth res.size()就说明这个深度还没有记录过任何节点此时当前节点就是第一次到达这个深度最右侧的节点把它塞进res。然后递归调用右子节点depth 1再递归调用左子节点depth 1。你可能会疑惑为什么“第一次到达”就一定是右视图能看到的节点因为深度优先搜索是沿着一条路径一直往下走的。当走到“右子树的最左侧”这一支时这一路上每个深度第一次访问的节点正好就是该深度最靠右的节点。反过来走左边路径时遇到已经记录的深度就直接跳过不会覆盖掉顺位更高的右节点。这里的关键就是递归顺序不能写反——先右后左一旦写成先左后右拿到的是左视图而不是右视图。DFS方案的代码更精简class Solution { public: vectorint rightSideView(TreeNode* root) { vectorint res; dfs(root, 0, res); return res; } void dfs(TreeNode* node, int depth, vectorint res) { if (!node) return; if (depth res.size()) { res.push_back(node-val); } // 注意顺序先右后左 dfs(node-right, depth 1, res); dfs(node-left, depth 1, res); } };有朋友可能会问为什么需要depth res.size()这个判断为什么不能直接每次res[depth] node-val最后留下来的就是最右边的理论上也可以但那样每个深度都会覆盖多次不够优雅而且需要额外处理深度大于当前数组长度的情况。用depth res.size()作为“新深度首次到达”的判定是一种很经典的DFS技巧也可以用在二叉树的左视图、俯视图等衍生问题上值得记住。2.3 为什么不用栈或其它数据结构其实也有用栈模拟递归的解法本质上和递归一样只是显式维护了一个vectorpairTreeNode*, int来模拟调用栈。但在本题中收益不大反而让代码变得冗长。力扣的考核重点是对遍历的掌握不是花哨的数据结构。能用递归就用递归递推层次短代码可读性高。如果遇到树特别深导致递归栈溢出的极端情况再考虑把DFS改成手动栈也不迟。再补充一个容易被人忽略的点如果使用Python需要注意递归深度限制默认只有1000超深二叉树会直接报RecursionError。力扣的测试数据一般不会卡这个但本地测试的时候自己生成一棵一万层的退化树两种写法就能感受到差异了。遇到这种情况BFS写法会更稳。3. 实操过程与核心环节实现3.1 环境准备与基础验证方法在开始刷题之前我建议先把环境准备好。不要直接在力扣网页的编辑器里硬写尤其是C选手建议在本地装一个支持C17的编译器配合一个简单的main函数手动构造几棵树做验证。这样你才能反复调试并且看到中间变量。我自己习惯在本地用VS Code写一个测试框架先定义一个TreeNode结构体再写一个根据数组建树的工具函数最后把右视图函数的结果打印出来。这样每写一个版本就可以跑一遍不用总是依赖力扣的在线判题。这一点很重要因为在线判题只能看到最终结果对不对看不到中间状态调试效率很低。下面是一个简易的建树和验证模板拿C举例#include iostream #include vector #include queue using namespace std; struct TreeNode { int val; TreeNode *left; TreeNode *right; TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; // 按层序序列化数组建树-1表示空节点 TreeNode* buildTree(vectorint nums) { if (nums.empty() || nums[0] -1) return nullptr; TreeNode* root new TreeNode(nums[0]); queueTreeNode* q; q.push(root); int idx 1; while (!q.empty() idx nums.size()) { TreeNode* node q.front(); q.pop(); if (nums[idx] ! -1) { node-left new TreeNode(nums[idx]); q.push(node-left); } idx; if (idx nums.size() nums[idx] ! -1) { node-right new TreeNode(nums[idx]); q.push(node-right); } idx; } return root; } int main() { vectorint nums {1, 2, 3, -1, 5, -1, 4}; TreeNode* root buildTree(nums); Solution s; vectorint res s.rightSideView(root); for (int v : res) cout v ; return 0; }这里的buildTree是按层序构造的-1表示空节点。实际做力扣题的时候不需要自己写建树逻辑因为后台已经处理好了但本地调试时这套工具能省很多时间。3.2 BFS实现层层扫描直接按照前面讲过的BFS模板实现即可。整个过程就是初始化队列 - 根节点入队 - 循环处理每一层 - 记录最右节点 - 收集下一层节点。我在纸上演算了一下常见测试用例[1,2,3,null,5,null,4]过程如下第一层队列是[1]size1唯一节点1就是最右节点入结果[1]。同时把2和3入队。第二层队列是[2,3]size2。i0时弹出2压入它的右子节点5i1时弹出3压入它的右子节点4。因为size-11所以记录3。结果变成[1,3]。第三层队列是[5,4]size2。i0弹出5i1弹出4记录4。结果变成[1,3,4]。这个例子的输出就是[1,3,4]。注意最右侧看到的不是2不是5而是4这是因为节点4在树的最右侧它盖住了左侧的5。这正是右视图题最容易理解错的地方——你看到的不是“右侧路径”上的节点而是“每一层最靠右”的节点。为了更加直观我建议调试时打印每一层的队列内容。你可以临时在for循环里加一行输出比如打印当前node-val和i的值。这样你会清晰地看到层与层之间是如何切换的。记得提交前把调试代码删掉就行。3.3 DFS实现深度优先定向采集DFS解法同样要能跑出相同结果。用前面那个dfs函数整个过程就是递归地从根开始先钻进右子树的最深处然后再处理左子树。每进入一个新的深度就把当前节点记录下来。因为“每个深度第一次被访问”一定发生在访问最右侧路径的过程中所以收集到的结果天然就是右视图。同样以[1,2,3,null,5,null,4]为例模拟一下递归从根节点1开始depth0res.size()0相等所以res[1]。先走右子树访问节点3depth1res.size()1相等res[1,3]。在3这个节点上先走它的右子树访问4depth2res.size()2相等res[1,3,4]。4没有左右孩子回溯到3再走3的左子树但3的左子树是null跳过。回溯到1再走1的左子树访问2depth1此时res.size()2不相等说明这一层已经记录过节点3了不能覆盖。2的右子节点5depth2res.size()3不相等跳过。结束res[1,3,4]。看到了吗关键就在于depth res.size()这个判断。它保证了我们“只记录每个深度第一个访问到的节点”并且由于右子树先被访问这个“第一个”就一定是本层最右侧的节点。这套逻辑写起来很简洁但如果不理解它背后的含义很容易在左子树先访问的版本里拿到完全错误的结果——那是左视图而不是右视图。3.4 时间复杂度和空间复杂度对比刷题不能光写代码还得能分析复杂度。这一题的两种解法时间上都是O(n)因为每个节点都恰好访问一次不存在重复访问。空间上需要区分BFS队列最大长度等于某一层的最大节点数最坏是满二叉树的最后一层约n/2个节点所以空间O(n)。DFS递归栈的深度等于树的高度主要是O(h)。如果树是平衡的h log n空间O(log n)如果树是退化的单链空间退化为O(n)。如果面试官问你“哪种更省空间”你可以回答在平衡树下DFS更省在极端退化树下两者差不多。这个回答能体现出你对数据结构的理解。4. 常见问题与排查技巧实录4.1 为什么总是报运行时错误热词里有一条“写二叉树程序时为什么总是报运行时错误”这个我太有体会了。这类报错十有八九是空指针解引用。刷二叉树题目时最容易出现问题的场景是直接访问node-left或node-right但没有先判断node是否为nullptr。比如DFS递归里如果先访问root-right再在下一层判断!root看起来好像没问题但如果你在递归之前没有做空指针判断一进来就对空指针操作编译器不报错才怪。我这里给一个实用建议在递归函数的第一行就写上判空这一行不要省。不管是BFS还是DFS所有入队的节点必须先判空再入队。尤其是BFS中如果你写了if (node-left) q.push(node-left)就不要再在pop之后检查node是否为空了因为入队前已经保证了非空。但有些乱七八糟的写法会把空节点也入队然后pop出来直接访问node-val那必然报错。遇到报错时我的排查顺序是这样的看报错信息里有没有member access within null pointer之类的字样有就是空指针。检查所有对指针的访问优先在递归函数开头加判空。用本地调试工具构造一个小型测试用例逐步打印每个节点的值定位到具体是哪个访问出了问题。很多人在初学阶段几乎每天都在跟运行时错误搏斗这是很正常的。二叉树的题目本质上就是链式结构的遍历指针用得好不好全靠这类题练出来。多写几次你就能形成条件反射访问任何节点之前先确认它是不是空。4.2 边界条件检查清单边界条件是最容易丢分的地方。右视图这道题至少需要检查以下几种情况空树直接返回空数组。如果根节点为nullBFS和DFS都应该直接跳过返回[]。只有根节点的树输出应该只有[root]左右子树都是空的情况下两种解法都能正确输出。单链树只有右子树或只有左子树如果只有右子树输出就是整条右链如果只有左子树输出是整条左链。很多人在只有左子树的情况下写错了输出成了空因为左子树的节点也能被看到但只要深度没记录过DFS就会写入而BFS则是每次把当层最后一个节点记录也能正确记录。左右子树高度不一致比如根节点只有左子树一路延伸五层右子树只有一个节点那么右视图的输出是根、左子节点、左左子节点……全部左链因为右子节点的深度只有一层覆盖不了后面的层。把这几类树在本地都测一遍如果能全部通过基本可以放心提交了。4.3 DFS递归深度陷阱与BFS稳定性刚才提到Python的递归深度默认限制是1000。如果题目给了一棵超过1000层的树DFS直接爆栈。虽然力扣的这题不会给这么夸张的树但如果你在刷题网站的自定义测试用例里看到“内存超限”或者“递归错误”多半是这个问题。这里有一个实用技巧如果真的遇到超大深度的数据可以把递归改成迭代用栈模拟。做法是维护一个vectorpairTreeNode*, int st用入栈的方式模拟递归调用。这种写法的好处是不受系统栈限制栈空间在堆上分配。不过说实话我刷了这么多题在力扣hot100的范围内很少需要这么做。笔试如果遇到直接选BFS反而更稳毕竟队列不受递归深度限制只要内存够大就没事。我还遇到过一个很隐蔽的错误DFS写法里如果先递归左子树再递归右子树但判断条件还是depth res.size()当左子树比右子树更深时某些深度记录的是左子树的节点但右视图要求记录的是这些深度最右侧的节点结果就全错了。这种“逻辑对但方向反”的问题往往在提交到第4个测试用例才发现。所以写DFS解右视图第一件事就是敲定递归顺序——先右后左没有例外。4.4 数据处理细节节点值可以为负数吗力扣这题的节点值范围是-100 Node.val 100也就是说节点值可以是负数。这本身不影响解题但很多人在本地写测试代码时用-1作为“空节点”的标记就出问题了。我前面的建树示例里用了-1表示空节点但默认节点值本身也可能为-1这时候如果输入数据恰好有-1节点就分不清了。实际做这题的时候树的输入是力扣后台通过序列化字符串给出的null表示空节点不涉及这个问题。但自己写测试框架时最好用独立的null标志或者用一个足够特殊的值比如INT_MIN或者改用TreeNode*数组来建树。这个细节虽然不影响提交但在本地debug时很影响心情提前规避掉能省不少事。5. 延伸思考与面试变体5.1 从右视图到左视图与俯视图右视图这道题做完左视图的答案几乎就是白送了——DFS解法只需要把递归顺序反过来先左后右BFS解法只需要记录每层第一个节点而不是最后一个。这个变体在面试里经常作为“追问”出现所以做这道题时一定要把左右两侧的写法都练一遍。实际验证下来改动量非常小但如果你只背了一个方向的模板考场上很容易卡住。俯视图会稍微复杂一些因为涉及水平坐标。它要求你想象自己从树的正上方往下看把同一水平位置的节点合并只输出每个水平位置最上面的节点。这个思路是给每个节点维护一个“水平距离”值根节点为0往左走减1往右走加1然后做BFS记录每个水平距离第一次出现的节点。这个题目在hot100里不常考但它能检验你是不是真正理解了树的“层”和“水平位置”这两个概念。既然右视图已经理解到“层”了再深入一层也不难。5.2 二叉树的深度如何影响右视图热词里有个“二叉树的深度”其实跟这道题关系密切。右视图的结果长度等于树的最大深度因为每一层都会贡献一个可见节点。如果你能把DFS解法里depth的变化想象成“纵向的尺子”那你很容易理解为什么res.size()能代表已记录的深度数量。很多人在递归里对“深度”的概念模糊最好的调试方式是打印每个节点访问时的depth值和res.size()对照看它们的变化。你会发现res.size()恰好等于已访问过的最大深度加一这就是深度向“结果长度”的映射。有一个有意思的实践把右视图的DFS写法套到“求二叉树最大深度”那道题上你会发现核心逻辑高度相似。区别只是一个是收集节点值一个是累加深度。这再次说明hot100的题目看似多样但背后基本遍历能力反复复用。5.3 面试扩展如果改成N叉树呢如果面试官让你做N叉树的右视图你能不能根据今天的方法快速迁移答案是可以的。BFS解法完全不用改核心逻辑只是在遍历子节点的时候用for循环遍历所有孩子节点最后一个孩子就是该层最右节点。DFS解法也只需要把先右后左改成“从最后一个孩子往前遍历”判断depth res.size()的逻辑完全不变。能在半小时内做好这种迁移说明你今天不是背了一道题而是真的掌握了“按层/按深度收集节点”的思考框架。5.4 这道题在日常开发中的意义有人可能会问力扣的二叉树题跟工作实际有什么关系我举一个例子在渲染一棵文件目录树的时候如果你只关心每层目录最右边的文件名或者某些界面里只需要展示每层最靠右的可见元素右视图的逻辑就是直接可用的。再比如在数据库里用邻接表表示层级结构时提取每一层的“边界元素”本质上也是层序遍历的变体数据处理。树的遍历并不只是面试题它是一类“先整体再局部”思维方式的训练场。你练得越熟面对现实的层级结构数据时就越有直觉。回到这道题本身两种解法没有绝对的优劣我更建议大家把DFS版本作为主要记忆点因为它代码短写起来快而且对递归能力的锻炼帮助很大。BFS版本作为保底方案遇到递归深度问题或者思路卡壳时立刻切换。刷题是为了形成肌肉记忆右视图这道题做扎实了后面的层序遍历类题目会顺很多。希望这篇整理能帮你省下一些撞破南墙的时间。