当前:LC101 · 对称二叉树 · 首次出现于 Day 20 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC101Easy二叉树递归镜像配对

对称二叉树

对称不是左右子树长得一样,而是把一边翻过镜子后能与另一边重合。递归每次比较一对镜像位置。

题目是什么

判断二叉树是否围绕根节点的中心轴镜像对称。

解决什么问题

同时验证节点值和左右结构的镜像关系。

核心结论

两空通过,一空失败,值不同失败;外侧与外侧、内侧与内侧交叉比较。

01交互算法精讲

先说结论:这道题到底解决什么

两棵子树的节点值看起来一样,为什么还不能说明二叉树对称?怎样让递归参数始终表示一对真正的镜像位置?

中心结论:两空通过,一空失败,值不同失败;外侧与外侧、内侧与内侧交叉比较。

读完必须能回答
  1. 1.对称树比较的是“相同方向”还是“交叉方向”,为什么?
  2. 2.为什么必须先区分两边都空、只有一边空和值不同三种情况?
  3. 3.当前一对节点相等后,为什么还要继续检查外侧与内侧两组孩子?
02交互算法精讲

完整题目与题意拆解

给定二叉树根节点 root,检查它是否轴对称。所谓轴对称,是指沿根节点中心轴翻转后,左右两侧的结构和值都能重合。

空树是对称的;非空树从 root.Left 与 root.Right 这第一对镜像位置开始比较。

  • 值相同是必要条件,但不是充分条件。
  • nil 的位置也属于树结构。
  • 比较方向必须交叉。
输入:[1,2,2,3,4,4,3]
输出:true

失败例:[1,2,2,null,3,null,3]
输出:false

站在镜轴两边的一对节点必须同时存在或同时为空;如果都存在,它们的值还必须相同。

继续向下时,左边节点的外侧孩子对应右边节点的外侧孩子,内侧孩子也互相对应。

外侧配对:left.Left ↔ right.Right;内侧配对:left.Right ↔ right.Left。不是同方向比较。
动画 1 · 题意扫描

对称不是左右两边长得差不多

中轴出现后,把左右孩子连成第一对镜像位置,并强调必须同时比较结构、值和方向。

Step 1/20%
第一对镜像位置
1
2
2
3
4
4
3
left=2 ↔ right=2
看完带走:对称判断的基本单位是一对镜像位置。
03交互算法精讲

第一层方案:暴力做法

可以复制一棵子树、翻转它,再与另一棵子树做相同树比较。思路直观,但创建了额外树结构,也把一个简单关系拆成两个问题。

也可以把左右遍历结果序列化后比较,但必须保留 nil 占位,空间为 O(n)。

mirror := invert(copy(root.Left))
return isSameTree(mirror, root.Right)
真正需要的不是生成镜像树,而是在遍历过程中按镜像位置直接配对。
动画 2 · 暴力重复

同方向比较为什么会走错

先演示 left.Left 对 right.Left 的错误连线,再切换到跨越中轴的交叉连线,展示两个问题的本质差异。

Step 1/20%
外侧镜像对
1
2
2
3
4
4
3
left.Left 3 ↔ right.Right 3
看完带走:镜像会交换左右方向,同方向递归检查的是相同树。

优化方向:双节点递归直接把“当前是否镜像”写进函数参数,每一步同时检查结构和值。

04交互算法精讲

整体地图:先做什么,再做什么

先把根节点视作一面镜子,问题从“遍历一棵树”改写成“同时检查镜子两侧的一对位置”。只要递归参数始终是一对镜像位置,判断条件就会自然浮现。

接着分别处理结构和值:两边都空说明这一对位置对称;只有一边空说明轮廓已经不同;都有节点时先比值,再交叉检查外侧和内侧。

  • 先确定一对镜像位置,而不是分别遍历两棵子树。
  • 结构不对称要比值不相等更早处理。
  • 外侧与内侧必须全部成立,不能只验证其中一组。
最重要的设计不是递归本身,而是让函数参数准确表达“这两个位置应该互为镜像”。
05交互算法精讲

把一个树问题改写成成对位置的镜像检查

定义 mirror(left,right):它回答的不是两棵树是否相同,而是 left 所在位置与 right 所在位置是否关于根轴镜像。初始调用是 root.Left 与 root.Right。

镜像会交换方向,因此 left.Left 应与 right.Right 配对,left.Right 应与 right.Left 配对。若写成 left.Left 与 right.Left,就把镜像问题误写成了相同树问题。

外侧配对像人的左手和镜中影像的右手;内侧配对则是靠近中轴的两个位置。
递归方向的交叉不是技巧,而是镜像定义直接推导出的结果。
动画 3 · 核心概念

外侧对外侧,内侧对内侧

两组彩色连线依次连接外侧和内侧孩子,让每次递归的参数关系在画面上保持可见。

Step 1/30%
外侧镜像对
1
2
2
3
4
4
3
left.Left 3 ↔ right.Right 3
看完带走:交叉配对完整表达了镜像的几何关系。
06交互算法精讲

先判结构,再判值,最后交叉递归

每一对位置按固定顺序处理:两边都 nil 返回 true;只有一边 nil 返回 false;两边都有节点但值不同返回 false。只有结构和值都通过,才继续向下一层。

随后计算 outer=mirror(left.Left,right.Right) 与 inner=mirror(left.Right,right.Left),返回 outer && inner。AND 表示每一处镜像关系都不能缺席。

  • 两空是这一条镜像分支正确结束。
  • 一空是结构失败,即使现有节点值与别处相同也无法补救。
  • 值相同只证明当前一对,不代表后代已经对称。
  • 外侧和内侧各负责树轮廓的一半。

空根直接返回 true。否则调用 mirror(root.Left, root.Right)。

mirror 按基础情况、值比较、交叉递归的顺序执行。这个顺序先保证访问安全,再保证当前节点,最后保证后代。

不要在当前值相同后直接返回 true。当前节点通过只说明这一对成立,下面两组镜像位置仍需验证。
动画 4 · 机制构建

基础情况先保护树的轮廓

动画依次展示两空成功、一空失败和值不同失败,使代码的判断顺序与结构判断一一对应。

Step 1/30%
当前值通过
1
2
2
3
4
4
3
2 == 2
看完带走:空位也是树结构的一部分,不能只比较已有节点值。
07交互算法精讲

为什么交叉递归完整覆盖了整棵树

递归不变量是:mirror(left,right) 检查的两个参数始终位于关于根轴互为镜像的位置。初始左右孩子显然满足它。

若两边都空,这一对轮廓一致;若一边空或值不同,当前镜像关系必然失败。两边都有相同值时,镜像定义只剩两项:外侧后代互为镜像,内侧后代互为镜像。

交叉递归恰好生成这两类子问题,因此由结构归纳可知,outer 与 inner 都为 true 当且仅当当前两棵子树互为镜像。

函数参数保持镜像关系,基础情况验证轮廓,交叉递归覆盖全部后代。
正确性抓手
  • 两空与一空规则完整刻画当前位置的结构是否镜像。
  • 当前值相同保证这一对节点内容一致。
  • 交叉递归分别验证外侧和内侧,二者都成立时整棵子树对称。
08交互算法精讲

完整执行过程

动画中央有一条镜轴。每一帧只送一对节点到镜面前,明确它们是外侧配对还是内侧配对。

  1. 1从根的左右孩子 (2,2) 开始,值相同且两边都存在。
  2. 2先检查外侧:左侧节点的左孩子 3 对右侧节点的右孩子 3。
  3. 3外侧孩子的对应空位也成对为空,因此外侧返回 true。
  4. 4再检查内侧:左侧右孩子 4 对右侧左孩子 4,同样成立。
  5. 5外侧与内侧都为 true,结果逐层汇总,整棵树被判为对称。
动画 5 · 完整执行

两组结果如何合并成最终答案

外侧和内侧检查分别返回后汇入 AND 门,只有两条支路全部通过,根节点才得到成功结论。

Step 1/40%
两空 → true
1
2
2
3
true
4
4
3
true
nil ↔ nil
看完带走:每一对镜像位置都成立,整棵树才对称。
09交互算法精讲

把动画和 Go 代码逐行对应

每一帧使用稳定的语义行 ID,不依赖容易漂移的数字行号。先观察分支为什么成立, 再看对应代码,而不是先背模板。

动画 6 · 代码映射

把镜像方向绑定到 Go 递归参数

代码高亮跟随当前镜像对,重点对齐两空、一空、值比较以及 outer/inner 两次交叉调用。

Step 1/40%
第一对镜像位置
1
2
2
3
4
4
3
left=2 ↔ right=2
看完带走:看参数顺序就能检查代码是否真的在做镜像比较。
Step 1
第一对来自根的左右孩子

根节点自己位于镜轴上,真正需要互相对应的是它的左右子树。

entry · value-check
Step 2
两个 2 相等,但还不能结束

对称还要求下面的结构和值继续按镜像位置对应。

both-nil · one-nil · value-check
Step 3
外侧:3 与 3 配对

它们是离镜轴最远、翻转后互相重合的位置。

mirror-outer
Step 4
外侧配对完整通过

两边都空说明结构在这里同步结束。

both-nil
Step 5
内侧:4 与 4 配对

它们都靠近镜轴,翻转后互相对应。

mirror-inner
Step 6
外侧和内侧都通过

当前值相同,并且所有镜像后代位置都一一对应。

return-mirror
Step 7
失败例:一边有节点,另一边为空

即使其他值相同,结构已经无法沿镜轴重合。

one-nil
Step 8
同方向比较检查的是相同,不是镜像

镜子会交换左右方向,递归参数必须交叉。

mirror-outer · mirror-inner
Step 9
一对节点就是一个递归问题

每次只做常数比较,递归栈取决于树高。

return-mirror
10交互算法精讲

完整 Go 提交代码与最小测试

完整 Go 解法
1func isSymmetric(root *TreeNode) bool {2    if root == nil { return true }3    return mirror(root.Left, root.Right)4}5 6func mirror(left, right *TreeNode) bool {7    if left == nil && right == nil { return true }8    if left == nil || right == nil { return false }9    if left.Val != right.Val { return false }10 11    outer := mirror(left.Left, right.Right)12    inner := mirror(left.Right, right.Left)13    return outer && inner14}
最小测试集合
// 标准对称树
fmt.Println(isSymmetric(buildTree([]any{1, 2, 2, 3, 4, 4, 3}))) // true
// 空位结构不对称
fmt.Println(isSymmetric(buildTree([]any{1, 2, 2, nil, 3, nil, 3}))) // false
// 空树与单节点天然对称
fmt.Println(isSymmetric(nil)) // true
fmt.Println(isSymmetric(&TreeNode{Val: 1})) // true
11交互算法精讲

正确性与复杂度

时间复杂度 O(n)

每个节点最多作为某个镜像对的一员被比较一次。

空间复杂度 O(h)

递归栈深度由树高决定;平衡树 O(log n),退化结构最坏 O(n)。

12交互算法精讲

最容易写错的地方

错误 1

同方向比较左右孩子,把镜像写成相同树。

错误 2

只比较值,忽略 nil 位置表达的结构。

错误 3

当前一对相等就提前返回 true。

错误 4

忽略递归栈空间。

13交互算法精讲

最后复盘:带走逻辑链

  1. 1.递归参数始终是一对镜像位置。
  2. 2.基础情况顺序:两空、一空、值不同。
  3. 3.后续方向必须交叉:外侧对外侧,内侧对内侧。
  4. 4.迁移题:LC100 相同树和 LC226 翻转二叉树。
面试表达
  1. 1.定义 mirror(left,right) 判断两个位置是否互为镜像。
  2. 2.两边都空返回 true,只有一边空或值不同返回 false。
  3. 3.继续交叉比较 left.Left/right.Right 和 left.Right/right.Left。
  4. 4.每个节点比较一次,时间 O(n),递归栈空间 O(h)。