Leetcode100: 94.二叉树中序遍历、104.二叉树最大深度、226.翻转二叉树

题目1:二叉树的中序遍历(LeetCode 94)

问题描述

给定一个二叉树的根节点 root,返回它的中序 遍历。中序遍历的顺序是:左子树 → 根节点 → 右子树

示例:

复制代码
输入: root = [1, null, 2, 3]
输出: [1, 3, 2]
解释: 
    1
     \
      2
     /
    3
中序遍历: 左(1的左子树空) → 1 → 右子树(2的左子树3 → 2 → 右子树空) = [1, 3, 2]

解题思路

核心思想: 递归(深度优先遍历)

  • 先遍历左子树

  • 然后访问根节点

  • 最后遍历右子树

Java代码(带详细注释)

java 复制代码
import java.util.ArrayList;
import java.util.List;

/**
 * LeetCode 94. 二叉树的中序遍历
 * 难度:简单
 * 给定一个二叉树的根节点 root,返回它的中序遍历。
 * 
 * 定义二叉树节点
 */
class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

public class BinaryTreeInorderTraversal {
    /**
     * 递归法实现中序遍历
     * 时间复杂度:O(n),空间复杂度:O(n)
     */
    public List<Integer> inorderTraversal(TreeNode root) {
        // 创建结果列表
        List<Integer> result = new ArrayList<>();
        // 调用递归函数
        inorder(root, result);
        return result;
    }
    
    /**
     * 辅助递归函数
     */
    private void inorder(TreeNode node, List<Integer> result) {
        // 递归终止条件:节点为空
        if (node == null) {
            return;
        }
        
        // 1. 递归遍历左子树
        inorder(node.left, result);
        
        // 2. 访问当前节点(将节点值加入结果)
        result.add(node.val);
        
        // 3. 递归遍历右子树
        inorder(node.right, result);
    }
}

流程图(Mermaid)

执行过程图解:

复制代码
二叉树:
      1
       \
        2
       /
      3

递归调用过程:
inorder(1)
  ├── inorder(1.left) → null → 返回
  ├── 访问 1 → result = [1]
  └── inorder(1.right) → 进入节点2
        ├── inorder(2.left) → 进入节点3
        │     ├── inorder(3.left) → null → 返回
        │     ├── 访问 3 → result = [1, 3]
        │     └── inorder(3.right) → null → 返回
        ├── 访问 2 → result = [1, 3, 2]
        └── inorder(2.right) → null → 返回

最终结果: [1, 3, 2] ✓

中序遍历的物理意义:

复制代码
对于二叉搜索树(BST),中序遍历的结果是升序排列的!

例如:
      5
     / \
    3   7
   / \   \
  2   4   8

中序遍历: [2, 3, 4, 5, 7, 8] ← 升序排列

问题1:root = [1, null, 2, 3] 这个东西叫什么?怎么画出树?

答: 这叫做层序遍历的序列化表示(Level-order Serialization)。它是LeetCode为了方便输入输出,用数组表示二叉树的方式。

规则: 按照从上到下、从左到右的顺序列出所有节点,null 表示该位置没有节点。

如果数组是 [3, 9, 20, null, null, 15, 7]

复制代码
         3
        / \
       9  20
      / \  / \
    null null 15 7

怎么画的?
索引0: 3 → 根
索引1: 9 → 3的左
索引2: 20 → 3的右
索引3: null → 9的左
索引4: null → 9的右
索引5: 15 → 20的左
索引6: 7 → 20的右

最终:
        3
       / \
      9  20
        /  \
       15   7

问题2:递归是什么意思?

答: 递归就是函数调用自己,就像一个套娃,或者照镜子时看到镜子里还有镜子。

最经典的解释:

复制代码
递归 = 递推 + 回归

"递":把问题分解成更小的子问题
"归":从最小的问题开始,一层层返回结果

生活类比:

复制代码
故事:从前有座山,山里有座庙,庙里有个和尚讲故事...
这就是递归!故事里套着同样的故事。

电影《盗梦空间》:
梦中梦 → 一层层进入 → 最底层 → 一层层出来
这就是递归的执行过程!

代码中的递归(中序遍历):

java 复制代码
private void inorder(TreeNode node, List<Integer> result) {
    if (node == null) {
        return;  // 这是最小的问题:空节点什么都不做
    }
    
    // "递"的过程:不断深入左子树
    inorder(node.left, result);  // 调用自己,处理更小的子树
    
    result.add(node.val);        // 访问当前节点
    
    // 从递归中"归"回来,再处理右子树
    inorder(node.right, result); // 再次调用自己
}

递归的四个要素:

  1. 终止条件:什么时候停止(node == null)

  2. 递归调用:调用自己(inorder(node.left, result))

  3. 当前处理:当前层做什么(result.add(node.val))

  4. 返回值:需要返回什么(void表示不返回)


问题3:inorderTraversal 翻译一下是什么意思?

答: 这是英语专业术语

单词 含义
inorder 中序(in + order,在中间的顺序)
Traversal 遍历(走过、访问每一个节点)
inorderTraversal 中序遍历

其他两种遍历:

  • preorderTraversal = 前序遍历(pre = 在前)

  • postorderTraversal = 后序遍历(post = 在后)


问题4:inorder(root, result); 是方法吗?

答: 是的,这是一个方法调用

java 复制代码
public List<Integer> inorderTraversal(TreeNode root) {
    List<Integer> result = new ArrayList<>();
    inorder(root, result);  // ← 这里调用了下面的方法
    return result;
}

private void inorder(TreeNode node, List<Integer> result) {
    // 这里是方法的具体实现
}

方法调用就是"找人干活":

java 复制代码
// 调用者(老板)
inorder(root, result);
//      ↑     ↑
//     参数1  参数2

// 被调用的方法(员工)
private void inorder(TreeNode node, List<Integer> result) {
    // 老板让我处理 node 和 result
    // 我用 node 和 result 来做具体工作
}

方法的结构:

复制代码
private void inorder(TreeNode node, List<Integer> result)
   ↑      ↑      ↑         ↑          ↑
修饰符  返回值  方法名    参数1      参数2

问题5:return 后面没有东西,是因为 void 的原因吗?

答: 完全正确!

java 复制代码
private void inorder(TreeNode node, List<Integer> result) {
    if (node == null) {
        return;  // ← 因为返回值类型是 void,所以什么都不返回
    }
    // ...
}

voidreturn 的关系:

返回值类型 return 用法 例子
void(无返回值) return; 或省略 if(条件) return;
void(有返回值) return 值; return 5;
java 复制代码
// void方法:可以省略return
private void method1() {
    // 执行完所有代码自动返回
}

// void方法:也可以写return(提前结束)
private void method2() {
    if (condition) {
        return;  // 提前结束,不执行后面的代码
    }
    // 后面的代码...
}

// 非void方法:必须有return
private int method3() {
    return 5;  // 必须返回一个int
}

在递归中的意义:

java 复制代码
if (node == null) {
    return;  // 到达空节点,停止递归,返回上一层
}
// 如果不写return,会继续执行,但node是null,访问node.left会报错!

问题6:inorder(node.left, result); 是什么意思?

答: 这是递归调用 ,意思是"去处理当前节点的左子树"。

它不是在赋值,而是在执行操作!

java 复制代码
// 错误理解 ❌
inorder(node.left, result);  // 把左节点的值赋给result?

// 正确理解 ✓
inorder(node.left, result);  // 去遍历左子树,把结果放到result里

详细图解:

复制代码
当前节点是 1:
    1  ← node
   / \
 null 2

执行 inorder(node.left, result):
意思是:去处理 node.left(即左子树)

左子树是 null → 递归终止,什么都不做
然后回到节点1,执行 result.add(1)
然后执行 inorder(node.right, result) → 去处理右子树

整个过程:
inorder(1)
  ├─→ inorder(1.left)    ← 去处理左子树
  │     └→ 发现是null,返回
  ├─→ result.add(1)       ← 访问当前节点
  └─→ inorder(1.right)   ← 去处理右子树
        └→ ...

不是"赋值",是"执行遍历动作":

java 复制代码
// 如果 node = 1
inorder(node.left, result);
// 等价于:去遍历以节点1的左孩子为根的整棵树
// 并把遍历结果添加到 result 列表中

问题7:JVM怎么知道它从哪个节点开始遍历?

答: JVM是通过递归调用栈来追踪的。每次递归调用,JVM都会记录当前执行到哪里。

调用栈(Call Stack)图示:

复制代码
假设树:
    1
   / \
  2   3

执行 inorder(1):

Step 1: 调用 inorder(1)
        JVM把"inorder(1)"压入栈顶
        栈:[inorder(1)]

Step 2: inorder(1) 调用 inorder(1.left) = inorder(2)
        栈:[inorder(1), inorder(2)]  ← 最上面是当前执行的

Step 3: inorder(2) 调用 inorder(2.left) = inorder(null)
        栈:[inorder(1), inorder(2), inorder(null)]

Step 4: inorder(null) 发现是null,return
        弹出 inorder(null)
        栈:[inorder(1), inorder(2)]

Step 5: 回到 inorder(2),执行 result.add(2)
        栈:[inorder(1), inorder(2)]

Step 6: inorder(2) 调用 inorder(2.right) = inorder(null)
        栈:[inorder(1), inorder(2), inorder(null)]

Step 7: inorder(null) 返回
        弹出 inorder(null)
        栈:[inorder(1), inorder(2)]

Step 8: inorder(2) 执行完毕,返回
        弹出 inorder(2)
        栈:[inorder(1)]

Step 9: 回到 inorder(1),执行 result.add(1)
        栈:[inorder(1)]

Step 10: inorder(1) 调用 inorder(1.right) = inorder(3)
         栈:[inorder(1), inorder(3)]

... 继续处理节点3

这就是JVM的"记忆"机制 —— 它通过栈记住每个递归调用的位置!

关键理解:

  • 每次递归调用,JVM都会在栈上创建一个新的帧(frame)

  • 每个帧记录了:方法名、参数、局部变量、执行位置

  • 当方法返回时,弹出栈顶帧,回到上一层继续执行

  • 所以JVM永远不会迷路


题目2:二叉树的最大深度(LeetCode 104)

问题描述

给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

示例:

复制代码
输入: root = [3, 9, 20, null, null, 15, 7]
输出: 3
解释: 
    3
   / \
  9  20
    /  \
   15   7
最大深度 = 3

解题思路

核心思想: 递归(分治法)

  • 最大深度 = max(左子树最大深度, 右子树最大深度) + 1

  • 递归终止条件:节点为空,返回0

Java代码(带详细注释)

java 复制代码
/**
 * LeetCode 104. 二叉树的最大深度
 * 难度:简单
 * 给定一个二叉树,找出其最大深度。
 */
public class MaximumDepthOfBinaryTree {
    public int maxDepth(TreeNode root) {
        // 递归终止条件:节点为空,深度为0
        if (root == null) {
            return 0;
        }
        
        // 计算左子树的最大深度
        int leftDepth = maxDepth(root.left);
        
        // 计算右子树的最大深度
        int rightDepth = maxDepth(root.right);
        
        // 当前节点的深度 = max(左子树深度, 右子树深度) + 1
        return Math.max(leftDepth, rightDepth) + 1;
    }
}

流程图(Mermaid)

执行过程图解:

复制代码
二叉树:
      3
     / \
    9  20
      /  \
     15   7

递归计算过程:
maxDepth(3)
  ├── leftDepth = maxDepth(9)
  │     ├── leftDepth = maxDepth(null) = 0
  │     ├── rightDepth = maxDepth(null) = 0
  │     └── 返回 max(0,0) + 1 = 1
  ├── rightDepth = maxDepth(20)
  │     ├── leftDepth = maxDepth(15)
  │     │     ├── leftDepth = maxDepth(null) = 0
  │     │     ├── rightDepth = maxDepth(null) = 0
  │     │     └── 返回 max(0,0) + 1 = 1
  │     ├── rightDepth = maxDepth(7)
  │     │     ├── leftDepth = maxDepth(null) = 0
  │     │     ├── rightDepth = maxDepth(null) = 0
  │     │     └── 返回 max(0,0) + 1 = 1
  │     └── 返回 max(1,1) + 1 = 2
  └── 返回 max(1,2) + 1 = 3

最终结果: 3 ✓

可视化深度计算:

复制代码
节点3: 深度 = max(节点9深度, 节点20深度) + 1
             = max(1, 2) + 1 = 3

节点20: 深度 = max(节点15深度, 节点7深度) + 1
              = max(1, 1) + 1 = 2

叶子节点: 深度 = max(0, 0) + 1 = 1

问题1:递归和遍历有什么区别?

答: 这是两个不同维度的概念:

概念 含义 例子
遍历 访问每个节点一次 中序遍历、前序遍历、后序遍历
递归 一种实现方式 用递归来实现遍历

它们是"方法"和"目的"的关系:

复制代码
遍历 是"目的"(我要访问所有节点)
递归 是"方法"(我用什么方式来实现)

类比理解:

复制代码
吃饭(目的) vs 用筷子吃(方法)
遍历(目的) vs 用递归实现遍历(方法)

四种遍历方式:

复制代码
1. 前序遍历(根左右)
2. 中序遍历(左根右)  ← 这些是"遍历顺序"
3. 后序遍历(左右根)
4. 层序遍历(按层)

每种都可以用递归或迭代实现!

代码对比:

java 复制代码
// 递归实现中序遍历
void inorder(TreeNode node) {
    if (node == null) return;
    inorder(node.left);    // 递归调用自己
    visit(node);
    inorder(node.right);
}

// 迭代实现中序遍历(用栈)
void inorder(TreeNode root) {
    Stack<TreeNode> stack = new Stack<>();
    TreeNode curr = root;
    while (curr != null || !stack.isEmpty()) {
        while (curr != null) {
            stack.push(curr);
            curr = curr.left;
        }
        curr = stack.pop();
        visit(curr);
        curr = curr.right;
    }
}

结论: 递归是一种实现遍历的方式,遍历是我们要完成的任务。


问题2:遍历用 for(xx : xx) 体现,那递归呢?

答: 递归没有特定的语法符号 ,它是通过函数调用自己来实现的。

java 复制代码
// 遍历(用for循环)
for (int i = 0; i < nums.length; i++) {
    // 访问每个元素
}
// 特点:有循环语法 for/while

// 递归(没有特殊语法)
void inorder(TreeNode node) {
    if (node == null) return;
    inorder(node.left);     // ← 这就是递归!调用自己
    visit(node);
    inorder(node.right);    // ← 递归调用自己
}
// 特点:函数内部调用自己

递归的"标志"就是:函数里调用自己!

java 复制代码
// 递归的识别方法:
void 方法名(参数) {
    if (终止条件) return;
    // ... 可能有一些处理
    方法名(子参数);  // ← 这里!调用自己!
    // ... 可能有一些处理
}

类比:

复制代码
for循环:就像在操场上跑圈,一圈一圈重复
        for → 跑一圈 → for → 跑一圈 → for → ...

递归:就像剥洋葱,一层一层剥开
       剥洋葱(大洋葱)
         → 剥掉外皮
         → 剥洋葱(小一点的洋葱)  ← 调用自己
           → 剥掉外皮
           → 剥洋葱(更小的洋葱)
             → ...直到没有洋葱可剥

题目3:翻转二叉树(LeetCode 226)

问题描述

给你一棵二叉树的根节点 root,翻转这棵二叉树,并返回其根节点。

示例:

复制代码
输入: root = [4, 2, 7, 1, 3, 6, 9]
输出: [4, 7, 2, 9, 6, 3, 1]
解释:
    4                   4
   / \                 / \
  2   7    翻转后→    7   2
 / \ / \             / \ / \
1  3 6 9            9  6 3  1

解题思路

核心思想: 递归(分治法)

  • 交换当前节点的左右子树

  • 递归翻转左子树

  • 递归翻转右子树

Java代码(带详细注释)

java 复制代码
/**
 * LeetCode 226. 翻转二叉树
 * 难度:简单
 * 给你一棵二叉树的根节点 root,翻转这棵二叉树,并返回其根节点。
 */
public class InvertBinaryTree {
    public TreeNode invertTree(TreeNode root) {
        // 递归终止条件:节点为空
        if (root == null) {
            return null;
        }
        
        // 1. 交换当前节点的左右子树
        TreeNode temp = root.left;
        root.left = root.right;
        root.right = temp;
        
        // 2. 递归翻转左子树
        invertTree(root.left);
        
        // 3. 递归翻转右子树
        invertTree(root.right);
        
        // 返回根节点
        return root;
    }
}

流程图(Mermaid)

执行过程图解:

复制代码
原二叉树:
      4
     / \
    2   7
   / \ / \
  1  3 6  9

翻转过程(从根开始):

Step 1: 翻转节点4
    4
   / \
  7   2      ← 交换了左右子树
 / \ / \
6  9 1  3

Step 2: 递归翻转左子树(节点7)
    4
   / \
  7   2
 / \ / \
9  6 1  3  ← 节点7的左右子树交换了

Step 3: 递归翻转右子树(节点2)
    4
   / \
  7   2
 / \ / \
9  6 3  1  ← 节点2的左右子树交换了

最终结果:
      4
     / \
    7   2
   / \ / \
  9  6 3  1 ✓

逐层翻转细节:

复制代码
原树:
        4
      /   \
     2     7
    / \   / \
   1   3 6   9

第1层(根节点4):
交换 2 和 7
        4
      /   \
     7     2
    / \   / \
   6   9 1   3

第2层(节点7):
交换 6 和 9
        4
      /   \
     7     2
    / \   / \
   9   6 1   3

第2层(节点2):
交换 1 和 3
        4
      /   \
     7     2
    / \   / \
   9   6 3   1

完成 ✓

问题1:invertTree(root.left);invertTree() 是方法吗?JVM怎么知道要翻转节点?

答: 是的,invertTree() 是一个方法 。JVM并不是"知道要翻转",而是按照你写的代码执行

java 复制代码
public TreeNode invertTree(TreeNode root) {
    if (root == null) return null;
    
    // 第1步:交换左右孩子
    TreeNode temp = root.left;
    root.left = root.right;
    root.right = temp;
    
    // 第2步:递归翻转左子树
    invertTree(root.left);   // ← 这里调用自己
    
    // 第3步:递归翻转右子树
    invertTree(root.right);  // ← 这里调用自己
    
    return root;
}

JVM执行流程(详细拆解):

复制代码
调用: invertTree(节点4)

执行过程(从代码行数看):

Line 1: 调用方法,root = 节点4
Line 2: root != null,继续
Line 3: temp = root.left (temp = 节点2)
Line 4: root.left = root.right (节点4的左孩子变成节点7)
Line 5: root.right = temp (节点4的右孩子变成节点2)
Line 6: 调用 invertTree(root.left) 
        → 此时 root.left 是节点7
        → 进入新的 invertTree(节点7)
        → 重复同样的过程...

Line 7: 上一行执行完毕,回来继续
Line 8: 调用 invertTree(root.right)
        → 此时 root.right 是节点2
        → 进入新的 invertTree(节点2)
        → 重复同样的过程...

Line 9: 返回 root

JVM的执行规则:

  1. 顺序执行:代码从上到下执行

  2. 方法调用:跳到方法体执行,执行完跳回来

  3. 递归调用:和普通方法调用一样,只是调用的方法是自己

所以JVM不知道"要翻转"这个概念,它只是在执行:

复制代码
交换 → 处理左子树 → 处理右子树 → 返回

完整的执行追踪:

复制代码
调用 invertTree(4):
  ├─ 交换 4 的左右孩子: 2 ↔ 7
  ├─ 执行 invertTree(4.left) → invertTree(7):
  │    ├─ 交换 7 的左右孩子: 6 ↔ 9
  │    ├─ 执行 invertTree(7.left) → invertTree(9):
  │    │    ├─ 9 无孩子,直接返回 9
  │    │    └─ 返回
  │    ├─ 执行 invertTree(7.right) → invertTree(6):
  │    │    ├─ 6 无孩子,直接返回 6
  │    │    └─ 返回
  │    └─ 返回 7
  ├─ 执行 invertTree(4.right) → invertTree(2):
  │    ├─ 交换 2 的左右孩子: 1 ↔ 3
  │    ├─ 执行 invertTree(2.left) → invertTree(3):
  │    │    ├─ 3 无孩子,返回 3
  │    │    └─ 返回
  │    ├─ 执行 invertTree(2.right) → invertTree(1):
  │    │    ├─ 1 无孩子,返回 1
  │    │    └─ 返回
  │    └─ 返回 2
  └─ 返回 4

树被成功翻转!✓

关键理解:

  • JVM是"傻瓜式"执行器,它只执行指令

  • 你写"交换左右",它就交换

  • 你写"递归翻转左子树",它就递归翻转左子树

  • 它不需要理解"翻转"的含义,只需要执行你写的代码

相关推荐
乂爻yiyao1 小时前
0. openems 部署与体验
java·openems
TanYYF1 小时前
spring ai入门教程一
java·人工智能·spring
掉鱼的猫2 小时前
用 ChatModel 构建 LLM 驱动的 Java 应用
java·llm
4154112 小时前
JTS 空间运算实战:线 × 线、线 × 面、面 × 面叠加分析
java·jts·叠加分析
.Hypocritical.2 小时前
数据结构笔记——链表成环/反转 + 有序二叉树(BST)构建、遍历、删除
java·数据结构
只会写代码2 小时前
一套开箱即用实体反射Lambda链式工具,彻底告别原生反射样板代码
java·程序员·源码
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第151题】【06_Spring篇】第11题:说一下 Spring Bean 的生命周期?
java·开发语言·后端·spring·面试
骑士雄师2 小时前
java面试题:jvm ,mybatis
java·jvm·mybatis
气泡音人声分离2 小时前
技术解析|均衡器(EQ)工作原理与实操指南:从频率拆分到听感优化
算法·均衡器·音频剪辑