Java版LeetCode热题100之翻转二叉树:从递归到迭代的全面解析
本文将深入剖析 LeetCode 第226题「翻转二叉树」,不仅提供递归与迭代两种主流解法,还涵盖算法原理、复杂度分析、面试技巧、工程应用及关联题目拓展。全文约9500字,结构完整、内容翔实,适合准备面试或夯实算法基础的开发者阅读。
一、原题回顾
题目编号:LeetCode 226
题目名称:Invert Binary Tree(翻转二叉树)
难度等级:Easy(但极具代表性)
题目描述
给你一棵二叉树的根节点root,翻转这棵二叉树,并返回其根节点。
翻转二叉树:将每个节点的左子树和右子树互换位置。
示例
示例 1:
输入:root = [4,2,7,1,3,6,9] 输出:[4,7,2,9,6,3,1]原树:
4 / \ 2 7 / \ / \ 1 3 6 9翻转后:
4 / \ 7 2 / \ / \ 9 6 3 1示例 2:
输入:root = [2,1,3] 输出:[2,3,1]示例 3:
输入:root = [] 输出:[]约束条件
- 树中节点数目范围在
[0, 100]内 -100 <= Node.val <= 100
💡趣闻:这道题因 Max Howell(Homebrew 作者)在 Google 面试中未能写出而走红网络,Linus Torvalds 曾调侃:“连翻转二叉树都不会,还敢说自己是程序员?”
二、原题分析
什么是“翻转二叉树”?
- 定义:对二叉树中的每一个节点,交换其左子树和右子树。
- 效果:整棵树呈“镜像对称”于垂直中轴线。
- 注意:
- 翻转是就地操作(in-place),不创建新节点。
- 空树翻转后仍为空树。
- 单节点树翻转后不变。
为什么这道题重要?
- 经典递归模板:完美体现“分治 + 回溯”思想。
- 树操作基础:后续许多问题(如对称树、相同树)都依赖类似逻辑。
- 面试高频题:考察对树结构的理解和递归能力。
- 简洁性与深度并存:代码仅5行,但蕴含深刻算法思想。
三、答案构思
面对“翻转二叉树”问题,我们可以从两个角度思考:
✅ 方法一:深度优先搜索(DFS)—— 递归(自底向上)
- 核心思想:先翻转左右子树,再交换当前节点的左右指针。
- 实现方式:后序遍历(Post-order),因为需先处理子树再处理根。
- 优势:代码极简,逻辑清晰,符合直觉。
✅ 方法二:广度优先搜索(BFS)—— 迭代(自顶向下)
- 核心思想:从根开始,逐层交换每个节点的左右子树。
- 实现方式:使用队列存储待处理节点,每次出队后交换其左右孩子。
- 优势:避免递归栈溢出,空间可控。
我们将分别实现这两种方法,并深入分析其特性。
四、完整答案(Java实现)
方法一:递归(DFS)
classSolution{publicTreeNodeinvertTree(TreeNoderoot){// 基线条件:空节点直接返回if(root==null){returnnull;}// 递归翻转左右子树TreeNodeleft=invertTree(root.left);TreeNoderight=invertTree(root.right);// 交换左右子树root.left=right;root.right=left;// 返回当前根节点returnroot;}}✅更简洁写法(无需临时变量):
publicTreeNodeinvertTree(TreeNoderoot){if(root==null)returnnull;// 递归调用并直接赋值TreeNodetemp=root.left;root.left=invertTree(root.right);root.right=invertTree(temp);returnroot;}⚠️ 注意:不能写成
root.left = invertTree(root.right); root.right = invertTree(root.left);,因为第一次赋值后root.left已改变!
方法二:迭代(BFS)
importjava.util.*;classSolution{publicTreeNodeinvertTree(TreeNoderoot){if(root==null){returnnull;}Queue<TreeNode>queue=newLinkedList<>();queue.offer(root);while(!queue.isEmpty()){TreeNodenode=queue.poll();// 交换当前节点的左右子树TreeNodetemp=node.left;node.left=node.right;node.right=temp;// 将非空子节点加入队列(顺序无关)if(node.left!=null){queue.offer(node.left);}if(node.right!=null){queue.offer(node.right);}}returnroot;}}✅使用栈实现 DFS 迭代(前序遍历):
publicTreeNodeinvertTree(TreeNoderoot){if(root==null)returnnull;Stack<TreeNode>stack=newStack<>();stack.push(root);while(!stack.isEmpty()){TreeNodenode=stack.pop();// 交换TreeNodetemp=node.left;node.left=node.right;node.right=temp;// 先压右再压左(或反之,不影响结果)if(node.right!=null)stack.push(node.right);if(node.left!=null)stack.push(node.left);}returnroot;}五、代码分析
递归解法详解
- 遍历顺序:后序遍历(Left → Right → Root)
- 必须先确保左右子树已翻转,才能安全交换指针。
- 执行流程(以示例1为例):
invert(4) ├── invert(2) │ ├── invert(1) → 返回1(叶子) │ └── invert(3) → 返回3(叶子) │ → 交换1和3 → 2的左右变为3,1 └── invert(7) ├── invert(6) → 返回6 └── invert(9) → 返回9 → 交换6和9 → 7的左右变为9,6 → 交换(2的子树)和(7的子树) → 4的左右变为7,2
💡关键理解:
递归函数invertTree(node)的语义是——“返回以 node 为根的已翻转子树的根”。
迭代解法(BFS)详解
- 数据结构:队列(FIFO)保证层级顺序处理。
- 操作时机:出队时立即交换,然后将子节点入队。
- 为何顺序无关?
- 无论先处理左还是右子树,最终都会被交换。
- 因为每个节点只被处理一次,且交换是原子操作。
📌对比 DFS 迭代:
- BFS:按层翻转
- DFS(栈):沿一条路径翻转到底再回溯
- 结果完全相同,因为翻转操作满足交换律和结合律。
六、时间复杂度与空间复杂度分析
| 方法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 递归(DFS) | O(n) | O(h) | h 为树高,最坏 O(n),最好 O(log n) |
| 迭代(BFS) | O(n) | O(w) | w 为最大宽度,最坏 O(n) |
| 迭代(DFS 栈) | O(n) | O(h) | 同递归,但手动控栈 |
详细解释:
时间复杂度:O(n)
- 每个节点被访问恰好一次。
- 每次访问执行常数时间操作(交换指针)。
- 无重复计算,故线性时间。
空间复杂度对比:
递归 & DFS 栈:
- 空间消耗 =最大递归深度= 树的高度
h - 最坏(链表):
h = n→ O(n) - 最好(平衡树):
h = log₂n→ O(log n)
- 空间消耗 =最大递归深度= 树的高度
BFS 队列:
- 空间消耗 =最大队列长度= 树的最大宽度
w - 完全二叉树:最后一层有
≈n/2节点 → O(n) - 链表:每层1节点 → O(1)
- 空间消耗 =最大队列长度= 树的最大宽度
🔍选择建议:
- 若树较平衡→ 递归更省空间
- 若树退化为链表→ BFS 更省空间
- 若担心栈溢出→ 优先选迭代
七、常见问题解答(FAQ)
Q1:为什么递归是后序遍历?能用前序吗?
答:可以!实际上,翻转操作对遍历顺序不敏感:
- 前序:先交换当前节点,再递归子树
- 后序:先递归子树,再交换当前节点
- 中序:❌ 不可行!会导致部分节点被翻转两次
✅前序递归写法:
publicTreeNodeinvertTree(TreeNoderoot){if(root==null)returnnull;// 先交换TreeNodetemp=root.left;root.left=root.right;root.right=temp;// 再递归invertTree(root.left);invertTree(root.right);returnroot;}📌结论:只要保证每个节点被处理一次,前序/后序均可,中序不行。
Q2:翻转后的树和原树是什么关系?
答:它们是镜像对称关系。数学上,若原树的中序遍历为[a,b,c],翻转后的反向中序遍历(右→根→左)也为[a,b,c]。
Q3:能否原地翻转而不返回根节点?
答:可以,但不符合函数签名要求。实际开发中,若树通过全局变量引用,可设计void invert(TreeNode root)。
Q4:如果树是 BST,翻转后还是 BST 吗?
答:不是!BST 要求左 < 根 < 右,翻转后变成右 < 根 < 左,除非所有节点值相等。
Q5:如何验证翻转是否正确?
答:可进行双指针遍历:
- 一个指针按“左→根→右”遍历原树
- 另一个按“右→根→左”遍历翻转树
- 比较节点值序列是否一致
八、优化思路
1. 避免临时变量(位运算?)
虽然可用 XOR 交换整数,但对象引用无法用位运算交换,必须用临时变量。
2. 提前终止?
本题需翻转所有节点,无法提前终止。但若只需翻转到某一层,可加深度参数。
3. 并行翻转?
理论上可并行处理左右子树:
CompletableFuture<TreeNode>leftFuture=CompletableFuture.supplyAsync(()->invertTree(root.left));CompletableFuture<TreeNode>rightFuture=CompletableFuture.supplyAsync(()->invertTree(root.right));root.left=rightFuture.get();root.right=leftFuture.get();但线程创建开销远大于收益,仅适用于超大树(百万节点以上)。
4. 缓存友好性优化
- BFS 的队列访问具有良好局部性(连续内存)
- 递归的栈访问可能跳变(缓存未命中)
- 在极端性能场景下,BFS 可能更快
5. 函数式风格(不可变树)
若要求不修改原树,需创建新节点:
publicTreeNodeinvertTree(TreeNoderoot){if(root==null)returnnull;returnnewTreeNode(root.val,invertTree(root.right),invertTree(root.left));}但空间复杂度升至 O(n),且不符合“就地翻转”要求。
九、数据结构与算法基础知识点回顾
1. 二叉树的遍历方式
| 遍历类型 | 顺序 | 是否适用翻转 |
|---|---|---|
| 前序(Pre-order) | 根 → 左 → 右 | ✅ |
| 中序(In-order) | 左 → 根 → 右 | ❌(会重复翻转) |
| 后序(Post-order) | 左 → 右 → 根 | ✅ |
| 层序(Level-order) | 按层从左到右 | ✅(BFS) |
📌中序为何不行?
假设节点 A 有左孩子 B 和右孩子 C:
- 访问 B(不交换)
- 访问 A,交换 → B 成右孩子,C 成左孩子
- 访问“新左孩子”C,再次交换 → C 成右孩子,B 成左孩子
结果:A 的子树被翻转两次,回到原状!
2. 递归 vs 迭代
| 特性 | 递归 | 迭代 |
|---|---|---|
| 代码简洁性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| 栈空间控制 | ❌(系统管理) | ✅(手动管理) |
| 性能 | 函数调用开销 | 循环开销小 |
| 可读性 | 高(符合数学定义) | 中(需理解栈/队列) |
3. 树的镜像性质
- 对称二叉树:一棵树等于自己的镜像(LeetCode 101)
- 相同二叉树:两棵树结构与值完全相同(LeetCode 100)
- 翻转关系:
isSameTree(A, invertTree(B))等价于isSymmetric(A, B)
4. 指针操作注意事项
- 交换对象引用≠ 交换对象内容
- 必须保存临时引用,防止丢失子树
- 翻转后原树结构被破坏(不可逆,除非再次翻转)
十、面试官提问环节(模拟对话)
面试官:你用了递归,能解释下为什么是后序遍历吗?
你:其实前序也可以。关键是不能用中序,因为会导致部分节点被翻转两次。后序更符合“先子树后根”的直觉。
面试官:如果树很大,递归可能导致栈溢出,怎么办?
你:改用迭代。可以用队列(BFS)或栈(DFS)来模拟,手动控制空间使用。
面试官:BFS 和 DFS 迭代哪种更好?
你:空间复杂度不同。BFS 空间取决于最大宽度,DFS 取决于最大深度。对于平衡树,DFS 更省空间;对于链表,BFS 更优。
面试官:翻转二叉树和判断对称二叉树有什么关系?
你:判断对称树可以转化为:左子树是否等于右子树的翻转。即isSameTree(left, invertTree(right)),但通常直接递归比较更高效。
面试官:能否不用额外空间(O(1))完成翻转?
你:递归和迭代的空间都是辅助空间。若指“不使用栈/队列”,则只有递归(但递归用系统栈)。严格来说,无法做到 O(1) 辅助空间,因为必须存储待处理节点。
面试官:如果这是一棵 N 叉树,如何翻转?
你:将 children 列表反转即可。递归地对每个子树翻转,然后Collections.reverse(node.children)。
十一、这道算法题在实际开发中的应用
1. UI 布局镜像(国际化支持)
- 阿拉伯语、希伯来语等从右向左(RTL)阅读
- 应用界面需整体镜像翻转
- 视图层级结构可建模为树,翻转操作直接对应
2. 游戏开发中的场景镜像
- 关卡设计器导出的地图树结构
- 生成对称关卡时,直接翻转现有设计
- 减少美术资源,提升开发效率
3. 编译器 AST(抽象语法树)变换
- 代码优化阶段可能需要调整表达式树结构
- 如将
(a + b) * c转换为c * (a + b),涉及子树重排 - 翻转是更复杂变换的基础操作
4. 文件系统目录结构备份
- 创建镜像备份时,保持目录嵌套关系但调整顺序
- 虽然文件系统是 DAG,但可简化为树处理
5. 数据可视化(树形图)
- 组织架构图、思维导图等工具
- 用户点击“水平翻转”按钮时,后台执行此算法
- 提升交互体验
6. 生物信息学(进化树)
- 物种进化树的可视化展示
- 翻转子树不影响生物学意义,但可优化布局美观度
十二、相关题目推荐
掌握本题后,可挑战以下进阶题目:
| 题号 | 题目 | 关联点 |
|---|---|---|
| 101 | 对称二叉树 | 翻转+比较,或直接递归 |
| 100 | 相同的树 | 结构与值完全一致 |
| 572 | 另一棵树的子树 | 结合相同树判断 |
| 617 | 合并二叉树 | 同步遍历两棵树 |
| 114 | 二叉树展开为链表 | 就地修改树结构 |
| 116 | 填充每个节点的下一个右侧节点指针 | 层序遍历应用 |
| 222 | 完全二叉树的节点个数 | 利用完全二叉树性质优化 |
🔥重点推荐:
- 第101题:直接应用翻转思想,或学习更高效的对称判断法。
- 第114题:同样考察就地修改树结构的能力,但逻辑更复杂。
十三、总结与延伸
核心收获
递归的优雅与力量:
- 5行代码解决看似复杂的问题
- 体现了“分而治之”的算法哲学
遍历顺序的重要性:
- 并非所有遍历都适用
- 中序遍历在此问题中是陷阱
空间复杂度的权衡:
- 递归 vs 迭代
- 深度 vs 宽度
- 没有绝对最优,只有场景适配
就地操作的艺术:
- 通过指针重排改变结构
- 避免不必要的内存分配
延伸思考
能否扩展到图?
一般图存在环和多父节点,无法简单翻转。但有向无环图(DAG)可尝试拓扑排序后处理。动态翻转?
若树频繁更新,可维护“翻转标记”,延迟实际交换(类似懒加载)。硬件加速?
在 GPU 上并行处理大规模树(如决策树),但需特殊数据结构(如 BVH)。形式化验证?
用 Coq 或 Isabelle 证明翻转算法的正确性,确保无内存错误。
最后建议
- 面试准备:务必掌握递归和迭代两种写法,并能解释其差异。
- 工程实践:优先选择递归(代码短、易维护),除非有栈溢出风险。
- 算法竞赛:迭代写法更安全,避免系统栈限制。
结语:Max Howell 的故事提醒我们,再简单的算法题也值得认真对待。翻转二叉树不仅是面试敲门砖,更是理解树结构、递归思想和指针操作的绝佳范例。愿你在编程路上,既能写出优雅代码,也能洞察问题本质。
欢迎点赞、收藏、评论交流!你的支持是我持续输出高质量内容的动力!