Files
leetcode-go/二叉树/39-对称二叉树.md

415 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: ["LeetCode", "二叉树", "深度优先搜索", "广度优先搜索", "递归", "迭代", "简单"]
create time: 2026-05-17 11:00
---
# 39-对称二叉树
## 题面
给你一个二叉树的根节点 `root`,检查它是否**轴对称**。
> [!question] 💡 什么是「轴对称」?
> 一棵二叉树轴对称意味着:如果沿根节点画一条垂直中线,**左半树是右半树的镜像翻转**。
>
> > [!note] 🪞 直观理解
> > - 根节点永远对称(自己是自己的镜像)
> > - 左子树的左孩子 = 右子树的右孩子(值相等)
> > - 左子树的右孩子 = 右子树的左孩子(值相等)
> > - 上述规则**递归地**适用于所有层
**示例 1:**
```
输入:root = [1,2,2,3,4,4,3]
输出:true
1 ← 根节点
/ \
2 2 ← 两层对称 ✅
/ \ / \
3 4 4 3 ← 三层对称 ✅
```
**示例 2:**
```
输入:root = [1,2,2,null,3,null,3]
输出:false
1 ← 根节点
/ \
2 2 ← 二层对称 ✅
\ \
3 3 ← 三层不对称!左边是左孩子,右边是右孩子 ❌
```
> [!warning] ⚠️ 关键陷阱
> 示例 2 看起来两棵子树的「形状和值」完全一样,但**对称比较的不是两个子树各自相同,而是它们的镜像关系**。左子树的空位对应右子树的位置,不能简单地判断"两棵子树相等"。
**约束:**
- 树中节点数目在范围 `[1, 1000]` 内
- `-100 <= Node.val <= 100`
**进阶:** 你可以运用递归和迭代两种方法解决这个问题吗?
---
## 思路
### 方法一:深度优先搜索 DFS(递归)⭐⭐
#### 核心洞察
> [!question] 🤔 思考
> 我们已经知道如何判断两棵树是否完全相等——同时遍历、逐节点比较。但「对称」不是比较两棵相同的树,而是比较两棵树是否为**镜像**。这两者有什么区别?
关键区别在于遍历顺序:
| 比较方式 | 左子树遍历 | 右子树遍历 |
|---------|----------|----------|
| **相等比较** | 先左后右 | 先左后右 |
| **镜像比较** | 先左后右 | **先右后左** |
所以判断对称的本质是:编写一个辅助函数 `isMirror(left, right)`,同时遍历两棵子树,但方向相反:
```
isMirror(left, right):
// 两个都是 nil → 对称
// 只有一个 nil → 不对称
// 值不等 → 不对称
// 值相等 → 交叉比较:
// isMirror(left.left, right.right) && isMirror(left.right, right.left)
```
#### 递归三要素
| 要素 | 内容 |
|------|------|
| **终止条件** | ① 双 nil → true;② 单 nil → false;③ 值不等 → false |
| **递归表达式** | `left.left` 与 `right.right` 镜像 + `left.right` 与 `right.left` 镜像 |
| **返回值** | 布尔值,表示两节点是否互为镜像 |
#### 执行流程图解
以对称树 `[] (1,2,2,3,4,4,3)` 为例,展示镜像比较的配对路径:
```mermaid
flowchart TD
S["isMirror(2L, 2R)\n值相等(2=2)"] --> P1["交叉对1:\nisMirror(3L, 3R)\n值相等(3=3)\n✅ true"]
S --> P2["交叉对2:\nisMirror(4L, 4R)\n值相等(4=4)\n✅ true"]
P1 --> COMBINE["2L↔2R 两边都✅\n→ true"]
P2 --> COMBINE
COMBINE --> ROOT["isMirror(root.Left, root.Right)\n整棵树对称 ✅🎉"]
style ROOT fill:#d4edda,stroke:#28a745,stroke-width:3px
style COMBINE fill:#d4edda,stroke:#28a745
style P1 fill:#cce5ff,stroke:#004085
style P2 fill:#cce5ff,stroke:#004085
linkStyle 0,1,2 stroke:#28a745,stroke-width:2px
```
再看不对称的 `[] (1,2,2,null,3,null,3)`:
```mermaid
flowchart TD
S["isMirror(2L, 2R)\n值相等(2=2)"] --> P1["交叉对1:\nisMirror(nil, 3R)\n左侧nil右侧非nil\n→ false ❌"]
S --> P2["交叉对2:\nisMirror(4L, nil)\n左侧非nil右侧nil\n→ false ❌"]
P1 --> FAIL["2L↔2R 至少一边❌\n→ false"]
P2 --> FAIL
FAIL --> ROOT["isMirror(root)\n左子树 != 右子树镜像\n→ 整棵不对称 ❌"]
style FAIL fill:#f8d7da,stroke:#721c24,stroke-width:2px
style ROOT fill:#f8d7da,stroke:#721c24,stroke-width:3px
style P1 fill:#fff3cd,stroke:#856404
style P2 fill:#fff3cd,stroke:#856404
```
#### 为什么是「交叉」而不是「平行」?
> [!info] 🧠 对称 vs 相等的本质差异
>
> ```
> 判断两棵树 A 和 B 是否「相等」:
> compare(A.left, B.left) AND compare(A.right, B.right)
> ↑ ↑
> 左 ↔ 左 右 ↔ 右
>
> 判断两棵树 A 和 B 是否「对称/镜像」:
> mirror(A.left, B.right) AND mirror(A.right, B.left)
> ↑ ↑ ↑ ↑
> A的左 ↔ B的右 A的右 ↔ B的左
> └── 镜面反射方向 ──┘
> ```
>
> 这就是为什么「相等」用 `compare(left, left) + compare(right, right)`,而「对称」必须用 `mirror(left, right) + mirror(right, left)`——**方向交叉**是关键!
### 方法二:广度优先搜索 BFS(队列迭代)⭐
#### 核心思想
既然递归用栈隐式保存了待比较的节点对,那么 BFS 可以用显式队列来存储待比较的节点对:**每次从队列取出两个节点,比较它们是否镜像,再将它们的镜像孩子对入队。**
```
queue = [(root.left, root.right)]
while queue 非空:
left, right = pop(queue)
if both nil: continue
if only one nil: return false
if values differ: return false
// 将镜像孩子对按交叉顺序入队
push(queue, (left.left, right.right)) // 外侧对
push(queue, (left.right, right.left)) // 内侧对
return true
```
#### 执行流程图
```mermaid
flowchart LR
A["初始: [(2L, 2R)]"] -->|"比较: 2=2 ✅"| B["入队: (3L,3R), (4L,4R)"]
B -->|"比较: 3=3 ✅"| C["入队: (nil,nil), (nil,nil)"]
C -->|"跳过双nil"| D["继续取 (4L,4R)"]
D -->|"比较: 4=4 ✅"| E["入队: (nil,nil), (nil,nil)"]
E -->|"全部完成"| F["队列为空\n返回 true ✅"]
style F fill:#d4edda,stroke:#28a745,stroke-width:3px
style B fill:#e3f2fd,stroke:#004085
style D fill:#e3f2fd,stroke:#004085
```
### 方法三:深度优先搜索 DFS(栈迭代)⭐
除了 BFS 用队列,DFS 也可以用显式栈模拟递归过程。
> [!comparison] ⚖️ 三种写法对比
> | 维度 | DFS 递归 | BFS 队列迭代 | DFS 栈迭代 |
> |------|---------|-------------|-----------|
> | 代码简洁度 | ⭐⭐⭐ 仅需 6 行 | ⭐⭐ 需维护队列 | ⭐⭐ 需维护栈 |
> | 空间复杂度 | O(h) 递归栈 | O(n) 队列最多存一层节点对 | O(h) 栈深度 |
> | 比较顺序 | 前序(当前对→外侧→内侧) | 逐层(自顶向下) | 前序(自定义) |
> | 面试推荐度 | **首选** | 直观易懂 | 展示底层理解 |
---
## 代码提示
### 递归伪代码
```
func isMirror(left, right *TreeNode) bool:
// 终止条件①: 两个都为空 → 对称
if left == nil and right == nil:
return true
// 终止条件②: 只有一个为空 → 不对称
if left == nil or right == nil:
return false
// 终止条件③: 值不等 → 不对称
if left.Val != right.Val:
return false
// 递归: 交叉比较(外侧对外侧,内侧对内侧)
return isMirror(left.Left, right.Right) and isMirror(left.Right, right.Left)
func isSymmetric(root *TreeNode) bool:
return isMirror(root.Left, root.Right)
```
### BFS 迭代伪代码
```
if root == nil: return true
queue = [(root.Left, root.Right)]
while len(queue) > 0:
left, right = pop(queue)
if left == nil and right == nil: continue
if left == nil or right == nil: return false
if left.Val != right.Val: return false
push(queue, (left.Left, right.Right))
push(queue, (left.Right, right.Left))
return true
```
---
## 技巧
> [!tip] 🔑 「对称」和「相等」是一组好基友
> LeetCode 100. 相同的树判断两棵树是否完全相等,LeetCode 101. 对称二叉树判断两棵树是否互为镜像。两者代码结构高度相似,唯一的区别就是**参数遍历的方向**——相等是 `(左,左)+(右,右)`,对称是`(左,右)+(右,左)`。对比练习效果极佳。
> [!tip] 🔑 Go 多值赋值的优雅应用
> 虽然这道题不需要交换操作,但在类似的树问题中,Go 的多值赋值能让你避开临时变量。更重要的是,递归的两个子调用写在 `and` 两侧,Go 会在第一个结果为 false 时短路——天然优化。
> [!info] 📊 复杂度速查
> | 方法 | 时间复杂度 | 空间复杂度 |
> |------|-----------|-----------|
> | DFS 递归 | **O(n)** — 每个节点对访问一次 | **O(h)** — h 为树高,最坏 O(n)(链状),平均 O(log n)(平衡树) |
> | BFS 队列迭代 | **O(n)** — 每对节点入队出队各一次 | **O(n)** — 最坏情况下队列可能存 O(n)/2 个节点对 |
> | DFS 栈迭代 | **O(n)** | **O(h)** |
> [!warning] ⚠️ 常见错误:忘记处理双 nil 情况
> 有些人在写递归时只写了「左 nil 或右 nil 返回 false」,这会误判一对双 nil 叶子。双 nil 应该返回 true——因为它们都是空节点,自然是对称的。
> [!note] 🧩 延伸思考
> - **相同的树**(LeetCode 100):比较思路几乎一致,只是参数方向不交叉
> - **二叉树的最大深度**(LeetCode 104):对称树可以看作是求"左子树深度 == 右子树深度且结构镜像"
> - **N 叉树的对称性**:扩展到 N 叉树时,需要判断第 i 个孩子和第 (n-1-i) 个孩子是否互为镜像
---
## 代码
### DFS 递归法(推荐)⭐⭐
```go
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func isSymmetric(root *TreeNode) bool {
// 空树对称
return isMirror(root, root)
}
// isMirror 判断两棵树是否互为镜像
func isMirror(left, right *TreeNode) bool {
// 两个节点都为空 → 对称
if left == nil && right == nil {
return true
}
// 只有一个为空 → 不对称
if left == nil || right == nil {
return false
}
// 值不相等 → 不对称
if left.Val != right.Val {
return false
}
// 交叉比较:外侧对外侧,内侧对内侧
return isMirror(left.Left, right.Right) && isMirror(left.Right, right.Left)
}
```
> [!success] ✅ 运行验证
> 这是 LeetCode 第 101 题,通过率约 59%。代码虽短,但包含了二叉树递归的经典范式——**定义函数语义 → 列出终止条件 → 写出递归表达式**。注意这里巧妙地把 `root` 作为两棵树的根同时传入 `isMirror(root, root)`,让根节点的左右子树成为首次比较的对象。
### BFS 队列迭代法(参考)⭐
```go
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func isSymmetric(root *TreeNode) bool {
if root == nil {
return true
}
// 使用切片模拟队列,存储待比较的节点对
type pair struct {
left, right *TreeNode
}
queue := []pair{{root.Left, root.Right}}
for len(queue) > 0 {
// 出队
pair := queue[0]
queue = queue[1:]
left, right := pair.left, pair.right
// 两个都为空 → 继续下一对
if left == nil && right == nil {
continue
}
// 只有一个为空 → 不对称
if left == nil || right == nil {
return false
}
// 值不等 → 不对称
if left.Val != right.Val {
return false
}
// 将镜像孩子对按交叉顺序入队
queue = append(queue, pair{left.Left, right.Right}) // 外侧对
queue = append(queue, pair{left.Right, right.Left}) // 内侧对
}
return true
}
```
> [!note] 🐹 设计选择说明
> 这里使用了自定义 `pair` 结构体来存储节点对,相比 `[][]*TreeNode` 或扁平化两个队列的方式更直观。Go 的匿名结构体语法让这种配对变得简洁——在 Python 中直接用元组 `(left, right)`,在 Java 中可能需要自定义 Pair 类。
### DFS 栈迭代法(参考)⭐
```go
/**
* Definition for a binary tree node.
* type TreeNode struct {
* Val int
* Left *TreeNode
* Right *TreeNode
* }
*/
func isSymmetric(root *TreeNode) bool {
if root == nil {
return true
}
// 用切片模拟栈,存储待比较的节点对
type pair struct {
left, right *TreeNode
}
stack := []pair{{root.Left, root.Right}}
for len(stack) > 0 {
// 弹栈
n := len(stack) - 1
pair := stack[n]
stack = stack[:n]
left, right := pair.left, pair.right
if left == nil && right == nil {
continue
}
if left == nil || right == nil {
return false
}
if left.Val != right.Val {
return false
}
// 注意:栈是 LIFO,所以要逆序入栈才能保证比较顺序
// 与递归的前序逻辑一致:先处理内侧再处理外侧
stack = append(stack, pair{left.Right, right.Left}) // 内侧对先入
stack = append(stack, pair{left.Left, right.Right}) // 外侧对后入
}
return true
}
```
> [!tip] 🐹 BFS vs DFS 迭代的关键区别
> BFS 用**队列**(FIFO)——先入队的先被弹出,实现逐层比较;DFS 迭代用**栈**(LIFO)——后入栈的先被弹出。在 DFS 迭代中,如果要模拟递归的「先处理外侧再处理内侧」的顺序,就必须把内侧对**先入栈**,这样它反而会被**后弹出**——这体现了栈的逆序特性。