给定一棵二叉树,检查它是否轴对称:左子树和右子树互为镜像,外侧结点两两相同,内侧结点两两相同。一个常见的简化写法是按层收集"存在的值"检查是否回文,但这会丢掉空位的位置信息。本文用一个具体反例和 30,000 组随机树的穷举测试,证明这种写法会把 34% 的非对称树误判为对称,再给出正确的镜像递归写法、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)
$ 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); // 外侧比外侧,内侧比内侧 } }
$ 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 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); }
$ 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); // 外侧比外侧,内侧比内侧 } };
$ 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。蓝色是正在比较的一对结点,绿色是已确认互为镜像的结点。每一帧都是把上面的解法在这组输入上真实执行记录下来的。