题目1:对称二叉树(LeetCode 101)
问题描述
给你一个二叉树的根节点 root,检查它是否轴对称。即:左子树和右子树互为镜像。
示例:
输入: root = [1, 2, 2, 3, 4, 4, 3]
输出: true
解释:
1
/ \
2 2
/ \ / \
3 4 4 3
解题思路
核心思想: 递归(双指针)
-
编写一个辅助函数
isMirror(left, right),判断两棵树是否互为镜像 -
互为镜像的条件:
-
两个节点都为
null→ true -
其中一个为
null→ false -
两个节点的值相等,且
left.left和right.right互为镜像,left.right和right.left互为镜像
-
Java代码(带详细注释)
java
/**
* LeetCode 101. 对称二叉树
* 难度:简单
* 给你一个二叉树的根节点 root,检查它是否轴对称。
*/
public class SymmetricTree {
public boolean isSymmetric(TreeNode root) {
// 空树是对称的
if (root == null) {
return true;
}
// 检查根节点的左右子树是否互为镜像
return isMirror(root.left, root.right);
}
/**
* 辅助方法:判断两棵树是否互为镜像
*/
private boolean isMirror(TreeNode left, TreeNode right) {
// 1. 两个节点都为空 → 对称
if (left == null && right == null) {
return true;
}
// 2. 其中一个为空 → 不对称
if (left == null || right == null) {
return false;
}
// 3. 值相等 且 左.left == 右.right 且 左.right == 右.left
return left.val == right.val
&& isMirror(left.left, right.right) // 左的左 和 右的右
&& isMirror(left.right, right.left); // 左的右 和 右的左
}
}
流程图(Mermaid)
执行过程图解:
1
/ \
2 2
/ \ / \
3 4 4 3
调用 isMirror(2, 2):
Step 1: left.val(2) == right.val(2) ✓
递归1: isMirror(3, 3)
├─ left.val(3) == right.val(3) ✓
├─ 递归1.1: isMirror(null, null) → true
├─ 递归1.2: isMirror(null, null) → true
└─ 返回 true
递归2: isMirror(4, 4)
├─ left.val(4) == right.val(4) ✓
├─ 递归2.1: isMirror(null, null) → true
├─ 递归2.2: isMirror(null, null) → true
└─ 返回 true
返回 true && true = true
最终: true ✓
问题1:return isMirror(root.left, root.right); 为什么是return?
答: 是的!这行代码就是返回一个方法调用的结果。
核心理解:
java
public boolean isSymmetric(TreeNode root) {
if (root == null) {
return true;
}
// 这行代码的意思是:
// 调用 isMirror() 方法,然后把它的返回值直接返回
return isMirror(root.left, root.right);
}
拆解执行过程:
Step 1: 调用 isMirror(root.left, root.right)
↓
Step 2: isMirror() 执行完毕,返回一个 boolean 值(true 或 false)
↓
Step 3: 把这个 boolean 值作为 isSymmetric() 的返回值
// 等价于:
boolean result = isMirror(root.left, root.right);
return result;
// 再等价于(分开写):
boolean result = isMirror(root.left, root.right);
if (result == true) {
return true;
} else {
return false;
}
为什么要这样写?
因为 isSymmetric() 的返回值类型是 boolean
isMirror() 的返回值类型也是 boolean
所以 isMirror() 的返回值可以直接作为 isSymmetric() 的返回值!
就像:
int add(int a, int b) {
return a + b; // 返回计算结果
}
boolean isPositive(int x) {
return x > 0; // 返回判断结果(true/false)
}
题目2:二叉树的直径(LeetCode 543)
问题描述
给定一棵二叉树,计算它的直径长度。直径是任意两个节点路径长度中的最大值。这条路径可能经过也可能不经过根节点。
示例:
输入: root = [1, 2, 3, 4, 5]
输出: 3
解释:
1
/ \
2 3
/ \
4 5
直径: 4-2-1-3 或 5-2-1-3,长度3
解题思路
核心思想: 递归 + 全局变量
-
对于每个节点,计算:
-
leftDepth:左子树的最大深度 -
rightDepth:右子树的最大深度 -
经过当前节点的路径长度 =
leftDepth + rightDepth
-
-
用全局变量
maxDiameter记录所有节点中最大的路径长度 -
返回当前节点的最大深度 =
max(leftDepth, rightDepth) + 1
Java代码(带详细注释)
java
/**
* LeetCode 543. 二叉树的直径
* 难度:简单
* 给定一棵二叉树,计算它的直径长度。
*/
public class DiameterOfBinaryTree {
// 全局变量,记录最大直径
private int maxDiameter = 0;
public int diameterOfBinaryTree(TreeNode root) {
// 空树直径是0
if (root == null) {
return 0;
}
// 计算深度,同时更新直径
calculateDepth(root);
return maxDiameter;
}
/**
* 辅助方法:计算树的深度,同时更新直径
* 返回值:当前树的最大深度
*/
private int calculateDepth(TreeNode node) {
// 空节点深度为0
if (node == null) {
return 0;
}
// 计算左右子树的深度
int leftDepth = calculateDepth(node.left);
int rightDepth = calculateDepth(node.right);
// 经过当前节点的路径长度 = 左深度 + 右深度
// 更新全局最大直径
maxDiameter = Math.max(maxDiameter, leftDepth + rightDepth);
// 返回当前节点的深度(到叶子节点的最长路径)
return Math.max(leftDepth, rightDepth) + 1;
}
}
流程图(Mermaid)
执行过程图解:
1
/ \
2 3
/ \
4 5
calculateDepth(4):
leftDepth = 0, rightDepth = 0
maxDiameter = max(0, 0+0) = 0
返回 1
calculateDepth(5):
leftDepth = 0, rightDepth = 0
maxDiameter = max(0, 0+0) = 0
返回 1
calculateDepth(2):
leftDepth = calculateDepth(4) = 1
rightDepth = calculateDepth(5) = 1
maxDiameter = max(0, 1+1) = 2 ← 路径 4-2-5,长度2
返回 max(1,1)+1 = 2
calculateDepth(3):
leftDepth = 0, rightDepth = 0
maxDiameter = max(2, 0+0) = 2
返回 1
calculateDepth(1):
leftDepth = calculateDepth(2) = 2
rightDepth = calculateDepth(3) = 1
maxDiameter = max(2, 2+1) = 3 ← 路径 4-2-1-3,长度3
返回 max(2,1)+1 = 3
最终: maxDiameter = 3 ✓
关键理解:
每个节点都可能是"路径的拐点"(最高点)
节点1作为拐点:路径长度 = 左深度(2) + 右深度(1) = 3
节点2作为拐点:路径长度 = 左深度(1) + 右深度(1) = 2
直径是经过所有节点中最大的那个路径!
所以用全局变量记录最大值。
题目3:二叉树的层序遍历(LeetCode 102)
问题描述
给你二叉树的根节点 root,返回其节点值的层序遍历。(即逐层地,从左到右访问所有节点)
示例:
输入: root = [3, 9, 20, null, null, 15, 7]
输出: [[3], [9, 20], [15, 7]]
解释:
3
/ \
9 20
/ \
15 7
解题思路
核心思想: BFS(广度优先搜索)+ 队列
-
使用队列存储当前层的所有节点
-
每次处理一层:取出队列中所有节点,将它们的值加入当前层列表
-
同时将它们的左右子节点加入队列,供下一层使用
-
当前层处理完后,将列表加入结果
Java代码(带详细注释)
java
import java.util.*;
/**
* LeetCode 102. 二叉树的层序遍历
* 难度:中等
* 给你二叉树的根节点 root,返回其节点值的层序遍历。
*/
public class BinaryTreeLevelOrderTraversal {
public List<List<Integer>> levelOrder(TreeNode root) {
// 创建结果列表
List<List<Integer>> result = new ArrayList<>();
// 边界检查
if (root == null) {
return result;
}
// 使用队列存储节点(FIFO)
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root); // 根节点入队
// 当队列不为空时,说明还有节点需要处理
while (!queue.isEmpty()) {
// 当前层的节点数量(队列中现有的节点数)
int levelSize = queue.size();
// 当前层的值列表
List<Integer> levelValues = new ArrayList<>();
// 处理当前层的所有节点
for (int i = 0; i < levelSize; i++) {
// 取出队首节点
TreeNode node = queue.poll();
// 将节点的值加入当前层列表
levelValues.add(node.val);
// 将子节点加入队列(供下一层处理)
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
// 当前层处理完毕,加入结果
result.add(levelValues);
}
return result;
}
}
流程图(Mermaid)
执行过程图解:
3
/ \
9 20
/ \
15 7
队列操作过程:
第1层(根节点):
队列: [3] ← 初始
levelSize = 1
处理: 取出3,加入层列表 [3]
加入子节点: 9, 20
队列变成: [9, 20]
结果: [[3]]
第2层(节点9和20):
队列: [9, 20]
levelSize = 2
处理节点9: 取出9,加入层列表 [9]
子节点为空
处理节点20: 取出20,加入层列表 [9, 20]
加入子节点: 15, 7
队列变成: [15, 7]
结果: [[3], [9, 20]]
第3层(节点15和7):
队列: [15, 7]
levelSize = 2
处理节点15: 取出15,加入层列表 [15]
子节点为空
处理节点7: 取出7,加入层列表 [15, 7]
子节点为空
队列变成: []
结果: [[3], [9, 20], [15, 7]]
最终: [[3], [9, 20], [15, 7]] ✓
为什么用队列?
层序遍历 = 先进先出(BFS)
队列的特点:
先入队的节点先处理
左孩子先入队 → 左孩子先处理
右孩子后入队 → 右孩子后处理
所以能保证从左
问题1:Queue<TreeNode> queue = new LinkedList<>(); 编译器怎么理解的?
答: 这是Java的多态 和面向接口编程!我来梳理完整的编译和运行过程。
代码解析:
Queue<TreeNode> queue = new LinkedList<>();
// ↑接口 ↑实现类
编译阶段(静态类型检查):
编译时:
编译器看到 queue 的类型是 Queue<TreeNode>
↓
编译器检查 LinkedList 是否是 Queue 的子类型
↓
是的!LinkedList 实现了 Queue 接口
↓
编译通过!
编译器只知道 queue 有 Queue 接口中定义的方法
(add, offer, poll, peek 等)
编译器不知道也不关心具体的实现类是 LinkedList
运行阶段(动态绑定):
运行时:
queue 实际上指向了一个 LinkedList 对象
当调用 queue.offer(node) 时:
JVM 发现 queue 实际是 LinkedList
↓
执行 LinkedList 中的 offer 方法
↓
多态!运行时会执行实际对象的真实方法
问题2:队列有哪些方法?
答: Java的 Queue 接口提供了以下主要方法:
队列方法分类:
| 操作 | 抛出异常 | 返回特殊值 | 说明 |
|---|---|---|---|
| 插入 | add(e) |
offer(e) |
向队列尾部添加元素 |
| 移除 | remove() |
poll() |
从队列头部移除并返回元素 |
| 检查 | element() |
peek() |
返回队列头部元素但不移除 |
详细说明:
java
Queue<Integer> queue = new LinkedList<>();
// 1. 插入元素(入队)
queue.offer(1); // 返回 true,推荐使用
queue.add(2); // 队列满时抛出异常
// 2. 移除元素(出队)
Integer head = queue.poll(); // 队列空时返回 null,推荐使用
Integer head2 = queue.remove(); // 队列空时抛出异常
// 3. 检查元素(不移除)
Integer peek = queue.peek(); // 队列空时返回 null,推荐使用
Integer peek2 = queue.element(); // 队列空时抛出异常
在层序遍历中用到的方法:
java
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root); // 入队:根节点入队
TreeNode node = queue.poll(); // 出队:取出队首节点
queue.isEmpty(); // 判断队列是否为空
完整示例:
java
Queue<String> queue = new LinkedList<>();
// 入队
queue.offer("A");
queue.offer("B");
queue.offer("C");
System.out.println(queue); // [A, B, C]
// 查看队首(不移除)
System.out.println(queue.peek()); // A
System.out.println(queue); // [A, B, C](没变)
// 出队
System.out.println(queue.poll()); // A
System.out.println(queue); // [B, C]
System.out.println(queue.poll()); // B
System.out.println(queue); // [C]
System.out.println(queue.poll()); // C
System.out.println(queue); // []
System.out.println(queue.poll()); // null(队列空了)