bigywater.tech
算法与数据结构 · LeetCode 101

LeetCode 101. 对称二叉树

给定一棵二叉树,检查它是否轴对称:左子树和右子树互为镜像,外侧结点两两相同,内侧结点两两相同。一个常见的简化写法是按层收集"存在的值"检查是否回文,但这会丢掉空位的位置信息。本文用一个具体反例和 30,000 组随机树的穷举测试,证明这种写法会把 34% 的非对称树误判为对称,再给出正确的镜像递归写法、5 种语言实现,以及外侧/内侧比较的分步演示。

34%
随机二叉树里,被"按层看非空值回文"这种陷阱写法误判为对称的比例(30,000 组里 10,202 组)
4 步
分步演示:根的两个孩子、外侧一对、内侧一对,逐步确认镜像,另附 5 种语言实现

题意与示例

本文只用自己的话概括题意,完整约束以官方页面为准。给定一棵二叉树,检查它是否轴对称——左子树和右子树互为镜像:外侧结点两两相同,内侧结点两两相同,结构和值都要一致。一共 6 组用例,覆盖:官方两个示例、空树、单结点、左右子树值相同但没有更深结构、根的两个孩子值不同:

真实实测 · 本机执行输出
[1,2,2,3,4,4,3]  →  true
[1,2,2,null,3,null,3]  →  false
[]  →  true
[1]  →  true
[1,2,2]  →  true
[1,2,3]  →  false

陷阱:只看每层的非空值是否回文,会漏掉空位信息

一个常见的简化写法:按层做 BFS,每一层只收集"存在的结点"的值(丢弃空位信息),检查这层数字正读和反读是否一样,如果每一层都是回文就认为树对称。这个写法漏掉了空位的位置信息,会把结构不对称、只是"存在的值恰好能拼成回文"的树误判为对称。下面是一个具体反例:根是 1,左孩子是 2(只有左孩子 3,没有右孩子),右孩子是 2(也只有左孩子 3,没有右孩子)——两侧的孩子都"向左延伸",形状并不镜像:

真实实测 · 本机执行输出
输入 [1,2,2,3,null,3,null]
镜像递归(正确)  →  false
只看每层非空值是否回文(陷阱写法)  →  true

这层的非空值列表是 [3, 3],读起来是回文,陷阱写法就判定"对称";但真正的镜像检查会发现:左子树的左孩子(3)要跟右子树的右孩子(空)比较,一个有值一个是空,结构不对称,正确答案是 False。为了知道这个反例有多常见,做了一次穷举式的随机测试:生成 30,000 棵随机二叉树(结点数 0 到 8,值取 0 到 3),分别用两种写法判断,统计陷阱写法把"实际不对称"的树误判为"对称"的次数:

真实实测 · 本机执行输出
随机生成 30,000 棵二叉树(结点数 0-8,值 0-3):
陷阱写法误判为对称(实际不对称) 10,202 组,占 34.0%
陷阱写法误判为不对称(实际对称)      0 组

34% 的随机树会被这种写法误判成对称,而且这种写法从来没有反过来把真正对称的树判断成不对称——它是一个"偏宽松"的必要条件,不是充分条件。原因是:只统计每层"存在"的值,等于丢弃了每个值具体挂在哪个位置(哪个父结点的左边还是右边),两个结构完全不同的子树也可能凑出同一组数字。

镜像递归:五种语言的实现

isSymmetric(root) 等价于判断 root.left 和 root.right 是否互为镜像:mirror(a, b)——两边都空返回真;只有一边空返回假;根值不同返回假;否则要求 mirror(a.left, b.right)(外侧比外侧)并且 mirror(a.right, b.left)(内侧比内侧)同时成立。时间 O(n),空间是递归深度 O(h)。Python、Java、Go、JavaScript、C++ 都是真实编译运行的,用同一组 6 个测试用例。

Python · 解法代码class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right def is_symmetric(root): def mirror(a, b): if a is None and b is None: # 两边都空:这一对互为镜像 return True if a is None or b is None: # 一边空一边不空:不可能镜像 return False if a.val != b.val: # 结点的值不同 return False return mirror(a.left, b.right) and mirror(a.right, b.left) # 外侧比外侧,内侧比内侧 if root is None: return True return mirror(root.left, root.right)
真实实测 · Python · 前几组用例的输出
$ python3 solution.py
[1,2,2,3,4,4,3]  →  true
[1,2,2,null,3,null,3]  →  false
[]  →  true
[1]  →  true
[1,2,2]  →  true
[1,2,3]  →  false
Java · 解法代码class TreeNode { int val; TreeNode left, right; TreeNode(int v) { val = v; } } class Solution { public boolean isSymmetric(TreeNode root) { if (root == null) return true; return mirror(root.left, root.right); } private boolean mirror(TreeNode a, TreeNode b) { if (a == null && b == null) return true; // 两边都空:这一对互为镜像 if (a == null || b == null) return false; // 一边空一边不空:不可能镜像 if (a.val != b.val) return false; // 结点的值不同 return mirror(a.left, b.right) && mirror(a.right, b.left); // 外侧比外侧,内侧比内侧 } }
真实实测 · Java · 前几组用例的输出
$ javac -d build Main.java && java -cp build Main
[1,2,2,3,4,4,3]  →  true
[1,2,2,null,3,null,3]  →  false
[]  →  true
[1]  →  true
[1,2,2]  →  true
[1,2,3]  →  false
Go · 解法代码type TreeNode struct { Val int Left, Right *TreeNode } func isSymmetric(root *TreeNode) bool { if root == nil { return true } var mirror func(a, b *TreeNode) bool mirror = func(a, b *TreeNode) bool { if a == nil && b == nil { // 两边都空:这一对互为镜像 return true } if a == nil || b == nil { // 一边空一边不空:不可能镜像 return false } if a.Val != b.Val { // 结点的值不同 return false } return mirror(a.Left, b.Right) && mirror(a.Right, b.Left) // 外侧比外侧,内侧比内侧 } return mirror(root.Left, root.Right) }
真实实测 · Go · 前几组用例的输出
$ go run solution.go
[1,2,2,3,4,4,3]  →  true
[1,2,2,null,3,null,3]  →  false
[]  →  true
[1]  →  true
[1,2,2]  →  true
[1,2,3]  →  false
JavaScript · 解法代码class TreeNode { constructor(val = 0, left = null, right = null) { this.val = val; this.left = left; this.right = right; } } function isSymmetric(root) { function mirror(a, b) { if (a === null && b === null) return true; // 两边都空:这一对互为镜像 if (a === null || b === null) return false; // 一边空一边不空:不可能镜像 if (a.val !== b.val) return false; // 结点的值不同 return mirror(a.left, b.right) && mirror(a.right, b.left); // 外侧比外侧,内侧比内侧 } if (root === null) return true; return mirror(root.left, root.right); }
真实实测 · JavaScript · 前几组用例的输出
$ node run.js
[1,2,2,3,4,4,3]  →  true
[1,2,2,null,3,null,3]  →  false
[]  →  true
[1]  →  true
[1,2,2]  →  true
[1,2,3]  →  false
C++ · 解法代码struct TreeNode { int val; TreeNode *left, *right; TreeNode(int v) : val(v), left(nullptr), right(nullptr) {} }; class Solution { public: bool isSymmetric(TreeNode* root) { if (!root) return true; return mirror(root->left, root->right); } private: bool mirror(TreeNode* a, TreeNode* b) { if (!a && !b) return true; // 两边都空:这一对互为镜像 if (!a || !b) return false; // 一边空一边不空:不可能镜像 if (a->val != b->val) return false; // 结点的值不同 return mirror(a->left, b->right) && mirror(a->right, b->left); // 外侧比外侧,内侧比内侧 } };
真实实测 · C++ · 前几组用例的输出
$ clang++ -std=c++17 -O2 -o bin sol.cpp && ./bin
[1,2,2,3,4,4,3]  →  true
[1,2,2,null,3,null,3]  →  false
[]  →  true
[1]  →  true
[1,2,2]  →  true
[1,2,3]  →  false
真实实测 · 本机执行输出
共 6 组用例(含边界),以 Python 结果为基准逐组比对:
  Python      通过 6/6
  Java        通过 6/6
  Go          通过 6/6
  JavaScript  通过 6/6
  C++         通过 6/6

随机对拍

Python 版本的递归实现另外和一个独立写的迭代版本做了随机对拍:迭代版用一个队列,里面放的是"应该互为镜像"的结点对,每次弹出一对,按同样的规则比较,并把 (a.left, b.right) 和 (a.right, b.left) 两对新组合推回队列。

真实实测 · 本机执行输出
随机对拍 30,000 组(结点数 0 到 9 的随机二叉树,值取 0/1),递归版与迭代版的结果全部一致,0 处不一致。

另外四种语言只在上面 6 组固定用例上验证过,没有做大规模随机对拍。

分步演示:外侧比外侧,内侧比内侧

下面的演示用官方示例 [1,2,2,3,4,4,3]:先比较根的两个孩子(2 和 2),再比较外侧的一对(左子树的左孩子 3,对右子树的右孩子 3),再比较内侧的一对(左子树的右孩子 4,对右子树的左孩子 4),三对都相同,返回 True。蓝色是正在比较的一对结点,绿色是已确认互为镜像的结点。每一帧都是把上面的解法在这组输入上真实执行记录下来的。

对称二叉树:外侧比外侧,内侧比内侧
正在比较的一对结点已确认互为镜像的结点发现不同

    参考与说明

    • 官方题目页:LeetCode 101. 对称二叉树。本文只用自己的话概括了题意,完整约束以官方页面为准。
    • 数据来自本机真实运行,五种语言的版本写在页脚。
    • 没有覆盖:迭代(用队列)的写法作为主解法展示;层序遍历建树时对更深、更不规则空洞结构的处理。
    京ICP备2026067502号-1京公网安备11010502063487号