资讯动态

翻转二叉树:递归、迭代与二叉树运行时错误的完整拆解

发布时间:2026/10/13 14:38:56 来源:尧图企业网站定制
刷力扣hot100题的时候我最先跳过的一道题就是它——翻转二叉树。题目短得可怜就是给你一棵二叉树的根节点 root把这棵树的左右子树全部互换一遍再返回根节点。往前翻这道题的时候很多人的第一反应是“就这”但它在力扣hot100里的热度一直居高不下面试考场上碰到它的概率也相当高。我第一次刷这道题的时候心里想的是这不就是一个递归交换左右子树吗确实核心逻辑就十行左右。但真正把它吃透之后我才意识到这道题背后藏着一整套二叉树的基本功递归遍历怎么写才不丢子树、迭代方案怎么用栈和队列替代隐式调用栈、空指针为什么会成为大多数二叉树报错的根源、以及“二叉树深度”和“递归栈深度”之间的纠缠关系。这篇文章就把这道题从里到外拆一遍连热词里大家最头疼的“写二叉树程序时为什么总是报运行时错误”一起解决掉。不管你是在力扣上刷题还是准备面试白板题这篇文章都值得完整看一遍。1. 题目拆解翻转二叉树到底在考什么1.1 题目描述与核心思路力扣原题给的定义非常干净给你一棵二叉树的根节点 root翻转这棵二叉树并返回其根节点。所谓的翻转就是把每个节点的左右子树互换。从视觉上看整棵树会像照镜子一样发生左右反向所以这道题在英文社区里也叫 Mirror Tree 或者 Invert Binary Tree。这里要提一个很多刷题新人容易忽视的点翻转的是“子树指针”不是“节点值”。意思是你不能只把每个节点的左右孩子 val 对调而是要把整棵子树引用都换位置。比如某个节点的左子树是三层右子树是单节点翻转后左边要挂原来的右树右边要挂原来完整的左树整个三层子树的内部结构也必须跟着翻完。所以这道题从本质上讲不是一道简单“交换”题而是一道“遍历并重组子树”的题。为什么这么说因为翻转这个动作发生在每个节点上而你需要用一种方式把整棵树的所有节点都访问到并且对每个节点执行左右交换。这意味着无论你用什么顺序遍历前序、后序、层序只要完整覆盖每个节点并且交换其左右孩子最终得到的结果都是一样的。这一点非常关键它直接解释了为什么这道题能同时有递归、队列、栈好几种解法而且结果完全相同。1.2 为什么翻转二叉树能进hot100说实话hot100里比这道题“看起来难”的题多了去了但翻转二叉树的地位非常特殊。它不考复杂算法不考动态规划只考察你对递归结构最核心的理解。面试官非常喜欢在这道题上做文章你写完了递归他会问“那不用递归行不行”你写完了迭代他会问“如果树深度特别大你的方案还能扛得住吗”你全部写完他还会顺着问“翻转后还是二叉搜索树吗”。这些追问的落点其实都在考察“遍历二叉树的能力”以及“对递归调用栈的理解”。换句话说翻转二叉树是一道完美的“敲门砖”题它能快速暴露一个人对树的递归直觉到底是真的还是背的。这也是为什么它常年排在力扣hot100里学长学姐推荐刷题攻略时也总是把它放在“二叉树专题第一天”的位置。2. 递归解法先想清楚出口再写交换2.1 递归函数的三要素写递归翻转我习惯用后序顺序先翻转左子树再翻转右子树最后交换当前节点的左右孩子。这样做的好处是你递归进入左右子树的时候当前节点的结构还没被破坏子树内部翻完再回来交换逻辑非常自然。递归函数有三个要素必须想清楚返回值返回翻转之后的子树根节点类型是 TreeNode。终止条件当前节点为 None 时直接返回 None。单层逻辑交换当前节点的左右孩子。其中终止条件是最容易写错的地方。很多人一上来就写root.left, root.right root.right, root.left完全没考虑root可能为空。力扣的测试用例里永远有空树这一跑运行时错误就来了。所以在递归函数的第一行写判空是二叉树题的保命习惯。2.2 完整代码与手动模拟执行过程后序递归的 Python 实现如下# Definition for a binary tree node. # class TreeNode: # def __init__(self, val0, leftNone, rightNone): # self.val val # self.left left # self.right right class Solution: def invertTree(self, root: TreeNode) - TreeNode: if root is None: return None # 先翻转左右子树 left self.invertTree(root.left) right self.invertTree(root.right) # 再交换当前节点的左右子树 root.left right root.right left return root这段代码看起来简洁但如果你只是在力扣上按“提交通过”就完事那它对你的帮助其实很有限。我强烈建议你拿一棵三层树在纸上把调用过程完整展开一遍。比如原树长这样1 / \ 2 3 / \ 4 5调用 invertTree(1) 后先进入 invertTree(2)节点2又先进入 invertTree(4) 和 invertTree(5)。4和5没有孩子直接返回自身。于是节点2交换左右孩子变成2 / \ 5 4继续回到节点1接着调用 invertTree(3)节点3没有孩子返回自身。最后节点1交换左右孩子左边挂上原来的3右边挂上翻转后的2。最终结果1 / \ 3 2 / \ 5 4这个过程走完一遍你对递归的“自底向上”会有一个很明显的体感。所谓的递归其实就是系统帮你维护了一个调用栈一层层往下钻钻到底再一层层往回返。每返一层就处理当前节点的交换。2.3 复杂度与递归深度问题时间复杂度是 O(n)因为每个节点只被访问一次。空间复杂度取决于递归调用栈的深度平衡树的情况下是 O(log n)但如果二叉树退化成一条链每个节点只有一个孩子那递归深度就变成了 n空间复杂度直接飙升到 O(n)并且 n 一大会触发 Python 默认的递归深度限制。说到二叉树的深度热词里也有这条。我实测过在力扣环境下递归版翻转二叉树的测试数据基本不会深到触发 RecursionError但如果你在本地用一条超长的链去测试就会看到类似RecursionError: maximum recursion depth exceeded的报错。这个问题在 4.3 节我还会详细说这里先记住一个结论递归方案很美但深度大的场景存在栈溢出风险迭代方案是必须掌握的替补方案。3. 迭代解法用队列和栈实现同样的翻转3.1 队列层序翻转BFS 思路面试官在看完你的递归解法后下一句大概率是“能不能不用递归写一个”。这时候你如果能快速写出一版队列层序翻转印象分会非常加。层序翻转的核心逻辑是把根节点放进队列然后循环弹出节点每弹出一个节点就交换它的左右孩子再把它的非空孩子继续入队。为什么这样能翻转整棵树因为队列保证了每个节点都会被访问一次而交换左右孩子这个操作只依赖当前节点本身不依赖树的整体顺序。from collections import deque class Solution: def invertTree(self, root: TreeNode) - TreeNode: if root is None: return None queue deque([root]) while queue: node queue.popleft() # 交换当前节点的左右孩子 node.left, node.right node.right, node.left # 孩子入队 if node.left: queue.append(node.left) if node.right: queue.append(node.right) return root这里有一个细节值得单独说交换之后再入队入队的是已经换过位置的孩子。下一轮处理的时候这些节点依然处于“左右孩子需要再交换”的状态所以整个循环的状态非常一致不需要额外标记。层序翻转给人最直观的感受就像一排人从头走到尾每经过一个人就把他和旁边的人换位置走完整个队列整排人的顺序就整个反过来了。树的层次结构在这里只是决定了访问顺序并不影响交换的正确性。3.2 栈模拟深度优先DFS 迭代版除了队列用栈也能完成同样的工作。递归本质上是隐式栈我们只是把系统帮忙维护的栈换成自己手写的显式栈。用栈的代码和队列几乎一模一样只把容器从 deque 换成 list弹出方式从 popleft 换成 pop。class Solution: def invertTree(self, root: TreeNode) - TreeNode: if root is None: return None stack [root] while stack: node stack.pop() node.left, node.right node.right, node.left if node.left: stack.append(node.left) if node.right: stack.append(node.right) return root用栈实现的遍历顺序接近深度优先但这道题根本不关心你是先深入左子树还是先处理右子树因为每个节点的交换都是独立操作。理解到这一层你会发现翻转二叉树在思路上其实非常简单你需要一种容器把节点按顺序收集起来每次弹出一个就交换然后继续收集它的孩子直到容器清空。3.3 递归和迭代怎么选我在实际刷题中总结了一套选择标准用一个表格对比一下对比项递归方案迭代方案代码量约10行简洁清晰约15行需要多考虑容器操作空间开销依赖系统调用栈最坏O(n)显式栈/队列同样最坏O(n)深度风险深树可能触发RecursionError不依赖系统栈稳定性更好面试效果体现对递归结构的理解体现对栈和队列的掌控推荐场景日常刷题、基础教学面试追问、生产环境代码我的建议是面试时先流畅写完递归版用两句话把递归出口和交换顺序说清楚紧接着主动说“我再写一个迭代版避免递归深度过大”。这样既展示了你的递归基本功也体现出了工程上对栈溢出的敏感度比被动等面试官追问要好得多。4. 运行时错误排查二叉树题的常见坑4.1 空指针访问最常见的报错来源关于热词里很多人问的“写二叉树程序时为什么总是报运行时错误”我先给一个最典型的错误案例# 错误示范没有判空直接访问 left def invertTree(self, root): root.left, root.right root.right, root.left self.invertTree(root.left) self.invertTree(root.right) return root这段代码在 root 为 None 的时候会抛AttributeError: NoneType object has no attribute left。力扣的测试数据里几乎必含空树所以这种写法一提交就会挂。根治方法就是第 2 节说的递归函数里第一行判空迭代版本里入队/入栈前判断孩子是否为空。养成了这个习惯二叉树题的运行时错误至少能少掉一大半。别觉得这个坑太基础我在本地 debug 别人的代码时见到的报错十有八九都是这类空指针访问。4.2 覆盖式交换导致丢子树第二种高频报错不是崩溃而是“结果不对”。典型写法是# 错误示范直接覆盖丢了原左子树 node.left node.right node.right node.left这里第一行把 node.left 覆盖成了原来的右子树原来的左子树引用就丢了第二行再把 node.right 赋值为 node.left结果是左右子树全部变成了原来的右子树。整棵树翻完左边和右边都是同一棵子树树形直接乱了。注意这种覆盖式错误是二叉树题里最经典的“表面看着没问题一跑就丢子树”的案例。本地调试时如果发现翻转后的树少了半边节点九成的元凶就是它。正确做法有两种。一种是 Python 的元组交换node.left, node.right node.right, node.left右侧先计算再统一赋值天然安全。另一种是手动引入临时变量tmp node.left; node.left node.right; node.right tmpC、Java 里就得这么写。面试时如果语言是 Python我推荐直接写元组交换一行搞定还不会出错。4.3 RecursionError 和 Time Limit Exceeded 的区别运行时错误里还有两类非常容易被混淆RecursionError和Time Limit Exceeded。RecursionError 是递归深度超过系统限制在 Python 里默认是 1000 层。树退化成链状的时候最容易触发解决思路有两种一是改成迭代写法二是使用 sys.setrecursionlimit 调高限制。但我个人不建议把调高限制当常规方案因为它只是把阈值往后推并没有根治深度问题生产环境里递归深度不可控时依然会爆栈。Time Limit Exceeded 绝大多数情况是死循环而不是“代码跑得慢”。二叉树题目里出现死循环通常是因为某个节点反复进入容器但没有任何终止条件比如你把交换后的孩子又重复加入了队列。我排查超时问题时通常先打印每个节点的访问顺序一旦发现同一节点出现两次基本就是处理顺序出了问题。4.4 二叉树调试排查清单围绕“为什么报运行时错误”我整理出一套非常顺手的排查顺序照着走一遍基本能解决问题先看是否有空树用例递归入口一定先判空迭代入容器前先判空。再看是否有覆盖赋值检查你是否在交换时丢失了原来的左右子树引用。接着看递归深度树是否可能退化成链状检测递归深度是否逼近语言限制。然后看容器操作队列有没有正确出队入队栈会不会无限重复加入同一节点。最后用本地最小用例复现构造一个三层小树打印节点访问顺序和你的预期对比。这套清单在处理其他二叉树问题时也一样通用建议收藏。5. 从翻转二叉树延伸出去对称、BST与面试变形5.1 翻转与对称二叉树的关系翻转二叉树做完之后有一个非常自然的延伸题判断一棵二叉树是否对称也就是力扣 101 题。对称二叉树的定义是“左子树和右子树互为镜像”从某种角度来说判断对称本质上就在比较一棵树和它翻转后的结果是否一致。def isSymmetric(root): def isMirror(t1, t2): if t1 is None and t2 is None: return True if t1 is None or t2 is None: return False return (t1.val t2.val and isMirror(t1.left, t2.right) and isMirror(t1.right, t2.left)) return isMirror(root, root)把翻转和对称放在一起对比学习非常划算。翻转是“生成一棵新树”对称是“判断两棵树的关系”但它们都建立在同一个基本功上递归地比较或调整左右子树的位置。5.2 翻转之后怎么在本地验证结果力扣上提交通过不代表你彻底理解了。如果你想在本地验证自己的翻转逻辑我建议写一个打印树结构的辅助函数把空节点也打印出来这样翻前翻后的变化一目了然。from collections import deque def print_tree(root): if root is None: print([]) return res [] queue deque([root]) while queue: node queue.popleft() if node: res.append(node.val) queue.append(node.left) queue.append(node.right) else: res.append(None) while res and res[-1] is None: res.pop() print(res)用这个函数在本地观察翻转前后的层序输出能帮你省掉大量依靠肉眼核对树结构的时间。我在本地验证树形题时这个辅助函数几乎是保底工具。5.3 面试官常见的变形追问围绕翻转二叉树我在面试中遇到过以及听朋友反馈过的高级追问大致有这么几种如果只翻转某一层的子树该怎么做比如只翻转第 3 层及以下上层不动。解法是递归时额外传深度参数在深度达到条件后再交换。如果某些节点没有左孩子或右孩子翻转逻辑要不要改不需要None 也正常参与交换这是递归出口天然处理掉的边界。翻转之后做中序遍历序是什么特征对于一棵二叉搜索树翻转后中序遍历会从升序变成降序说得清楚这条很能体现你对遍历和顺序的理解。如何用 Morris 遍历实现 O(1) 空间复杂度的翻转这是进阶版利用叶子节点的空指针线索来遍历不借助栈和队列空间复杂度压到常数级。如果树的节点非常多递归挂了迭代方案的极限在哪里常规栈方案空间仍然是 O(n)如果希望空间更优还是要走 Morris 路线。其中 Morris 遍历和热词里的“线索二叉树”关系很大核心思想是让叶子节点的空闲指针指向某种遍历顺序下的后继节点从而不需要额外容器。笔试面试一般不会强制要求到这个深度但能主动提出来说明你对“二叉树的遍历”理解已经超出了背模板的层次。5.4 翻转二叉搜索树时发生了什么这里单独拎出来说因为二叉搜索树BST被翻转后会有一个很强的反直觉现象翻转后的树不再是一棵二叉搜索树。比如一棵 BST4 / \ 2 6 / \ / \ 1 3 5 7翻转后变成4 / \ 6 2 / \ / \ 7 5 3 1你观察根节点 4左孩子是 6右孩子是 2完全破坏了“左小右大”的规则。但这个翻转后的结构作为二叉树依然是合法的只是 BST 的有序性没了。这就引出一个高频追问如何保持翻转后仍然是 BST单靠交换节点做不到你需要根据 BST 的中序遍历特性来重建BST 中序遍历是升序翻转后中序变成降序所以可以按降序序列重新构造一棵 BST。这道题把翻转、遍历、BST 性质全部串起来是连环追问的经典套路。5.5 场景化联想翻转在真实开发中的应用最后说说这道题在实际业务里的意义免得总觉得刷题和上班是两码事。我在前端项目里做过一个树形组件的镜像布局需求是把左侧树形菜单整体平移到右侧同时子节点展开方向反过来。当时我第一反应就是用类翻转的思路递归遍历整棵树按镜像规则重新组织节点顺序。后来做双栏对照组件的时候右侧目录要和左侧目录呈镜像关系我也没有单独调接口而是直接把左侧的数据结构通过翻转思路转换生成。树结构的基本功真的会以你想象不到的方式出现在日常开发里。所以别觉得这道题短就轻视它递归、迭代、栈、队列、层序遍历、深度优先、空指针防护、BST 性质这一整套知识全都浓缩在翻转二叉树这十行代码里。hot100 选它当常客不是因为你写得快而是因为它足够浓缩。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑