漯河市网站建设_网站建设公司_前端工程师_seo优化
2026/1/17 15:03:18 网站建设 项目流程

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),不创建新节点。
    • 空树翻转后仍为空树。
    • 单节点树翻转后不变。

为什么这道题重要?

  1. 经典递归模板:完美体现“分治 + 回溯”思想。
  2. 树操作基础:后续许多问题(如对称树、相同树)都依赖类似逻辑。
  3. 面试高频题:考察对树结构的理解和递归能力。
  4. 简洁性与深度并存:代码仅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:

  1. 访问 B(不交换)
  2. 访问 A,交换 → B 成右孩子,C 成左孩子
  3. 访问“新左孩子”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题:同样考察就地修改树结构的能力,但逻辑更复杂。

十三、总结与延伸

核心收获

  1. 递归的优雅与力量

    • 5行代码解决看似复杂的问题
    • 体现了“分而治之”的算法哲学
  2. 遍历顺序的重要性

    • 并非所有遍历都适用
    • 中序遍历在此问题中是陷阱
  3. 空间复杂度的权衡

    • 递归 vs 迭代
    • 深度 vs 宽度
    • 没有绝对最优,只有场景适配
  4. 就地操作的艺术

    • 通过指针重排改变结构
    • 避免不必要的内存分配

延伸思考

  • 能否扩展到图
    一般图存在环和多父节点,无法简单翻转。但有向无环图(DAG)可尝试拓扑排序后处理。

  • 动态翻转
    若树频繁更新,可维护“翻转标记”,延迟实际交换(类似懒加载)。

  • 硬件加速
    在 GPU 上并行处理大规模树(如决策树),但需特殊数据结构(如 BVH)。

  • 形式化验证
    用 Coq 或 Isabelle 证明翻转算法的正确性,确保无内存错误。

最后建议

  • 面试准备:务必掌握递归和迭代两种写法,并能解释其差异。
  • 工程实践:优先选择递归(代码短、易维护),除非有栈溢出风险。
  • 算法竞赛:迭代写法更安全,避免系统栈限制。

结语:Max Howell 的故事提醒我们,再简单的算法题也值得认真对待。翻转二叉树不仅是面试敲门砖,更是理解树结构、递归思想和指针操作的绝佳范例。愿你在编程路上,既能写出优雅代码,也能洞察问题本质。

欢迎点赞、收藏、评论交流!你的支持是我持续输出高质量内容的动力!

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

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

立即咨询