二叉树第一周总结
周一:二叉树的理论基础(Java 版)
本周我们开始讲解二叉树。在《二叉树,你该了解这些!》中讲解了二叉树的理论基础。
有同学会把红黑树和二叉平衡搜索树(Balanced Binary Search Tree)搞混了,其实红黑树就是一种二叉平衡搜索树,它们不是独立的概念。
例如,在 C++ 中 map
、multimap
、set
、multiset
的底层实现是红黑树。
而在 Java 中,TreeMap
和 TreeSet
的底层实现也是红黑树,所以 Java 的有序集合同样基于平衡二叉搜索树。
✅ Java 中二叉树节点的定义
public class TreeNode {int val;TreeNode left;TreeNode right;// 构造函数:初始化值为 x 的节点,左右子节点默认为 nullTreeNode(int x) {val = x;}// 可选:带左右子节点的构造函数TreeNode(int x, TreeNode left, TreeNode right) {val = x;this.left = left;this.right = right;}
}
🔍 关于构造函数的说明:
有些同学对
TreeNode(int x) { val = x; }
不太理解,这是 Java 中的构造方法。Java 的类(包括
TreeNode
)可以定义构造函数来简化对象创建。有构造函数时:
TreeNode node = new TreeNode(9); // 简洁明了
没有构造函数时:
TreeNode node = new TreeNode(); node.val = 9; node.left = null; node.right = null;
显然更繁琐,所以定义构造函数是良好编程习惯。
🌟 附加知识点:Morris 遍历
在介绍前序、中序、后序遍历时,除了递归和迭代方法外,还有一种高效的遍历方式:Morris 遍历。
- 优点:将非递归遍历的空间复杂度降到 O(1),不需要栈或队列。
- 原理:利用叶子节点的空指针(
left
或right
为null
)建立临时线索,实现空间优化。 - 缺点:代码复杂,破坏了树的结构(临时修改指针),结束后需恢复。
- 面试情况:几乎不考,属于算法进阶内容,感兴趣可自行查阅学习。
⚠️ 提示:我本人也未深入研究 Morris 遍历,建议初学者优先掌握递归与迭代写法。
周二:递归三要素与 N 叉树遍历(Java 版)
在《二叉树:一入递归深似海,从此 offer 是路人》中讲到了递归三要素:
- 确定递归函数的参数和返回值
- 确定终止条件
- 确定单层递归的逻辑
掌握了递归思想后,不仅能解决二叉树的前、中、后序遍历,还能轻松扩展到 N 叉树 的遍历问题。
🔧 推荐练习题(LeetCode):
- 589. N 叉树的前序遍历
- 590. N 叉树的后序遍历
✅ 提示:N 叉树节点定义如下(Java):
class Node {public int val;public List<Node> children;public Node() {}public Node(int _val) { val = _val; }public Node(int _val, List<Node> _children) {val = _val;children = _children;} }
前序遍历模板(递归):
public void preorder(Node root, List<Integer> res) {if (root == null) return;res.add(root.val); // 中for (Node child : root.children) {preorder(child, res); // 遍历所有子节点} }
周三:栈实现迭代遍历(Java 版)
在《二叉树:听说递归能做的,栈也能做!》中,我们开始用栈模拟递归过程,即所谓的迭代法。
有同学发现:前后序遍历中,空节点是否入栈的写法不同。
其实都可以,但空节点不入栈更清晰,也符合动画演示逻辑。
✅ 示例:前序遍历(空节点入栈 vs 不入栈)
❌ 空节点入栈(不推荐,逻辑稍乱)
class Solution {public List<Integer> preorderTraversal(TreeNode root) {Stack<TreeNode> stack = new Stack<>();List<Integer> result = new ArrayList<>();stack.push(root);while (!stack.isEmpty()) {TreeNode node = stack.pop();if (node != null) {result.add(node.val); // 中stack.push(node.right); // 右stack.push(node.left); // 左}// else continue; // 空节点跳过}return result;}
}
✅ 空节点不入栈(推荐,逻辑清晰)
class Solution {public List<Integer> preorderTraversal(TreeNode root) {Stack<TreeNode> stack = new Stack<>();List<Integer> result = new ArrayList<>();if (root == null) return result;stack.push(root);while (!stack.isEmpty()) {TreeNode node = stack.pop();result.add(node.val); // 中if (node.right != null) stack.push(node.right); // 右if (node.left != null) stack.push(node.left); // 左}return result;}
}
🤔 递归 vs 迭代:谁更优?
对比项 | 递归 | 迭代(栈模拟) |
---|---|---|
时间复杂度 | O(n) | O(n) |
空间复杂度 | O(h),h 为树高(系统栈开销) | O(h),手动维护栈 |
可读性 | 高,逻辑清晰 | 中,需要理解栈操作 |
安全性 | 低,深度大时可能栈溢出 | 高,可控性强 |
实际开发建议 | 尽量避免,尤其在参数复杂、调用深时 | 推荐用于生产环境,更稳定 |
💡 总结:递归方便程序员,难为了机器;迭代难为了程序员,方便了机器。
周四:统一写法的迭代遍历(Java 版)
在《二叉树:前中后序迭代方式的写法就不能统一一下么?》中,我们使用空节点标记法实现了前、中、后序遍历的统一写法。
例如:中序遍历统一写法
class Solution {public List<Integer> inorderTraversal(TreeNode root) {List<Integer> result = new ArrayList<>();Stack<TreeNode> stack = new Stack<>();if (root != null) stack.push(root);while (!stack.isEmpty()) {TreeNode node = stack.pop();if (node != null) {// 按照 “右 -> 中 -> null -> 左” 顺序入栈if (node.right != null) stack.push(node.right);stack.push(node);stack.push(null); // 标记中间节点if (node.left != null) stack.push(node.left);} else {// 遇到 null,说明下一个节点是需要处理的中间节点node = stack.pop();result.add(node.val);}}return result;}
}
三种迭代遍历:
/ 前序遍历顺序:中-左-右,入栈顺序:中-右-左
class Solution {public List<Integer> preorderTraversal(TreeNode root) {List<Integer> result = new ArrayList<>();if (root == null){return result;}Stack<TreeNode> stack = new Stack<>();stack.push(root);while (!stack.isEmpty()){TreeNode node = stack.pop();result.add(node.val);if (node.right != null){stack.push(node.right);}if (node.left != null){stack.push(node.left);}}return result;}
}// 中序遍历顺序: 左-中-右 入栈顺序: 左-右
class Solution {public List<Integer> inorderTraversal(TreeNode root) {List<Integer> result = new ArrayList<>();if (root == null){return result;}Stack<TreeNode> stack = new Stack<>();TreeNode cur = root;while (cur != null || !stack.isEmpty()){if (cur != null){stack.push(cur);cur = cur.left;}else{cur = stack.pop();result.add(cur.val);cur = cur.right;}}return result;}
}// 后序遍历顺序 左-右-中 入栈顺序:中-左-右 出栈顺序:中-右-左, 最后翻转结果
class Solution {public List<Integer> postorderTraversal(TreeNode root) {List<Integer> result = new ArrayList<>();if (root == null){return result;}Stack<TreeNode> stack = new Stack<>();stack.push(root);while (!stack.isEmpty()){TreeNode node = stack.pop();result.add(node.val);if (node.left != null){stack.push(node.left);}if (node.right != null){stack.push(node.right);}}Collections.reverse(result);return result;}
}
❓ 是否必须使用统一写法?
不需要! 哪种写法你记得住、写得顺,就用哪种。
但必须掌握至少一种迭代写法,因为面试官常会在你写出递归后追问:“能写成迭代吗?”
周五:层序遍历(广度优先搜索)
在《二叉树:层序遍历登场!》中,我们介绍了层序遍历,即图论中的**广度优先搜索(BFS)**在二叉树上的应用。
✅ Java 层序遍历模板
class Solution {public List<List<Integer>> levelOrder(TreeNode root) {List<List<Integer>> result = new ArrayList<>();if (root == null) return result;Queue<TreeNode> queue = new LinkedList<>();queue.offer(root);while (!queue.isEmpty()) {int levelSize = queue.size();List<Integer> level = new ArrayList<>();for (int i = 0; i < levelSize; i++) {TreeNode node = queue.poll();level.add(node.val);if (node.left != null) queue.offer(node.left);if (node.right != null) queue.offer(node.right);}result.add(level);}return result;}
}
✅ 推荐练习题:
- 102. 二叉树的层序遍历
- 107. 二叉树的层序遍历 II(从下到上)
- 199. 二叉树的右视图
- 637. 二叉树的层平均值
- 429. N 叉树的层序遍历
- 515. 在每个树行中找最大值
✅ 技巧:层序遍历的框架固定,只需在每层遍历时添加特定逻辑即可。
周六:翻转二叉树(Java 版)
在《二叉树:你真的会翻转二叉树么?》中,我们剖析了这道经典题目。
❌ 递归中序遍历的问题
直接使用标准中序遍历(左 → 中 → 右)会导致某些节点被翻转两次:
// 错误示例:会导致重复翻转
void invert(TreeNode root) {if (root == null) return;invert(root.left); // 左swap(root.left, root.right); // 中invert(root.right); // 右 → 此时 right 是原来的 left,又被翻转一次!
}
✅ 修正版中序递归(非常规写法)
class Solution {public TreeNode invertTree(TreeNode root) {if (root == null) return null;invertTree(root.left); // 先翻转左子树swapChildren(root); // 翻转当前节点invertTree(root.left); // 注意!这里仍是 left,因为刚被 swap 过return root;}private void swapChildren(TreeNode root) {TreeNode temp = root.left;root.left = root.right;root.right = temp;}
}
⚠️ 虽然能工作,但这已不是标准中序逻辑,容易混淆,不推荐。
✅ 迭代法中序统一写法(可行)
使用栈 + 空节点标记法,可以安全实现中序翻转:
class Solution {public TreeNode invertTree(TreeNode root) {Stack<TreeNode> stack = new Stack<>();if (root != null) stack.push(root);while (!stack.isEmpty()) {TreeNode node = stack.pop();if (node != null) {// 按右 -> 中 -> null -> 左 入栈if (node.right != null) stack.push(node.right);stack.push(node);stack.push(null);if (node.left != null) stack.push(node.left);} else {// 处理中间节点node = stack.pop();swapChildren(node); // 翻转左右子树}}return root;}private void swapChildren(TreeNode root) {TreeNode temp = root.left;root.left = root.right;root.right = temp;}
}
✅ 为什么可以?因为是用栈控制访问顺序,而不是靠指针移动,避免了重复翻转。
总结:本周知识图谱(Java 版)
本周我们系统学习了二叉树:
- 理论基础:二叉树种类、存储方式、遍历方式
- 遍历方法:
- 递归(前/中/后序)
- 迭代(栈模拟,统一与非统一写法)
- 层序遍历(队列,BFS)
- 核心思想:
- 递归三要素
- 栈与队列的应用
- 指针与引用的操作
- 经典题目:翻转二叉树,综合运用各种遍历方式
✅ 学习建议:
- 先掌握递归写法(易理解)
- 再学习迭代写法(面试加分)
- 最后尝试统一写法或 Morris 遍历(进阶)
- 多刷题,形成“遍历 + 处理”的思维模式