对称二叉树 图解题解
不用造镜像树——让递归同时握住左右两个节点交叉核对就行。
对称就像两个人站在镜子两侧做同一套动作:左边举左手、右边必须举右手,才算真正镜像。不需要先复制出一个镜像人再逐动作比,只要派一个裁判同时盯着两人——左的左和右的右是外侧一对、左的右和右的左是内侧一对,两对都一致才算这一层通过,然后裁判继续往下一层走。
这道题到底在问什么
- 输入
- [1,2,2,3,4,4,3,5,6,7,8,8,7,6,5]
- 输出
- true(对称)
- 输入
- [1,2,2,null,3,null,3]
- 输出
- false(不对称)
最优解:为什么这么做
一句话答案:LeetCode 101 对称二叉树的标准解是递归比较镜像对:定义辅助函数 mirror(a, b),让 a 的左孩子对 b 的右孩子、a 的右孩子对 b 的左孩子交叉比较,值不等或一空一有立即返回 false。每个节点只被比较一次,时间 O(n),空间为递归栈深 O(h),h 是树高。
这道题真正在问什么
题目给一棵二叉树,问它是否关于根节点这条中轴左右镜像对称。注意「对称」不等于「左右子树长得一样」:想象把树沿中轴对折,左半边最外侧的节点要正好压在右半边最外侧的节点上。题目示例 [1,2,2,null,3,null,3] 里,两棵子树都是「2 带一个右孩子 3」,长得一模一样,答案却是 false——对折之后两个 3 的位置根本对不上。要判断的是位置加数值的双重对应。
为什么直接比较左右子树行不通
最直觉的做法是逐层取出节点检查每层是否回文,可行但要小心补 null 占位,写起来啰嗦。更本质的观察是:整棵树对称,当且仅当左子树与右子树互为镜像。而镜像的含义是外侧对外侧、内侧对内侧——照镜子时,左子树的最外缘正对右子树的最外缘。所以比较必须交叉进行:左边节点的左孩子,要去和右边节点的右孩子配对。这一步交叉是全题的题眼,也是它区别于「判断两棵树是否相同」的地方。
递归函数为什么要同时带两个节点
单参数的递归表达不了「这棵子树和另一棵子树互为镜像」这种跨树关系,所以要定义双参数函数 mirror(a, b),含义是「以 a、b 为根的两棵子树互为镜像」,入口调用 mirror(root.left, root.right)。它的子问题结构和自身完全一致:a 与 b 镜像,等价于两者值相等,且 a.left 与 b.right 镜像、a.right 与 b.left 镜像。问题规模每递归一层就缩小到下一层的镜像对,天然收敛。
三个判断条件为什么缺一不可
递归里按顺序判三种情况:a、b 都为空返回 true——对折后两边都没有东西,天然对齐,这是递归的触底出口;一空一有返回 false——结构已经错位,这是只比较数值时最容易漏掉的情况;值不相等返回 false。三关都过了,才继续往里递归两组镜像对。正确性来自「处处相等」:任何一对镜像位置不匹配,false 会沿递归链一路短路传回根部,整棵树即判不对称。
复杂度怎么算,哪些边界会翻车
时间 O(n):每个节点至多参与一次镜像比较,n 为节点数。空间 O(h):递归栈深等于树高,平衡树约 O(log n),退化成链最坏 O(n)。最常见的错误是把递归写成 mirror(a.left, b.left)——不交叉,验证的就是「左右子树相同」而非镜像;其次是只比完根的左右孩子就下结论,上层对称不代表深层对称,必须递归到底。空树按约定返回 true。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3镜子的核心是外侧对外侧、内侧对内侧:照镜子时,左子树最外缘正对右子树最外缘。所以递归里 a 的左要去和 b 的右比,a 的右去和 b 的左比——这一步交叉是全题的题眼。
- 4从 (根左 ⟷ 根右) 这一对镜像节点出发先看清这棵树:根 1 是对称轴(绿)。沿这条轴对折,左半边的 2-3-4-5-6-7-8 该和右半边的 2-4-3-8-7-6-5 一一照镜子。下面从根的左、右孩子这一对开始,逐对比较。
- 5cmp: 节点2 与 节点2 是否相等?照镜子时,左半边的某个节点 2 应当正对右半边对称位置的节点 2。先比较它们的值——只有值先相等,才有资格继续往里比。
- 62 == 2,继续比 外⟷外、内⟷内值相等!把这一对标蓝(已确认对称)。但还没完——镜像要求外侧对外侧、内侧对内侧:接下来要比 2 的左孩子 ⟷ 2 的右孩子,以及 2 的右 ⟷ 2 的左。
- 7cmp: 节点3 与 节点3 是否相等?照镜子时,左半边的某个节点 3 应当正对右半边对称位置的节点 3。先比较它们的值——只有值先相等,才有资格继续往里比。
- 83 == 3,继续比 外⟷外、内⟷内值相等!把这一对标蓝(已确认对称)。但还没完——镜像要求外侧对外侧、内侧对内侧:接下来要比 3 的左孩子 ⟷ 3 的右孩子,以及 3 的右 ⟷ 3 的左。
- 9cmp: 节点5 与 节点5 是否相等?照镜子时,左半边的某个节点 5 应当正对右半边对称位置的节点 5。先比较它们的值——只有值先相等,才有资格继续往里比。
- 105 == 5,继续比 外⟷外、内⟷内值相等!把这一对标蓝(已确认对称)。但还没完——镜像要求外侧对外侧、内侧对内侧:接下来要比 5 的左孩子 ⟷ 5 的右孩子,以及 5 的右 ⟷ 5 的左。
- 11空 ⟷ 空:镜像边界情形,返回 true这对叶子 5 与 5 之下都是空(没有孩子)。镜像边界规则——两边都空就算对称成立,递归在这里触底返回 true,这条分支安全收口。
- 12cmp: 节点6 与 节点6 是否相等?照镜子时,左半边的某个节点 6 应当正对右半边对称位置的节点 6。先比较它们的值——只有值先相等,才有资格继续往里比。
- 136 == 6,继续比 外⟷外、内⟷内值相等!把这一对标蓝(已确认对称)。但还没完——镜像要求外侧对外侧、内侧对内侧:接下来要比 6 的左孩子 ⟷ 6 的右孩子,以及 6 的右 ⟷ 6 的左。
- 14空 ⟷ 空:镜像边界情形,返回 true这对叶子 6 与 6 之下都是空(没有孩子)。镜像边界规则——两边都空就算对称成立,递归在这里触底返回 true,这条分支安全收口。
- 15cmp: 节点4 与 节点4 是否相等?照镜子时,左半边的某个节点 4 应当正对右半边对称位置的节点 4。先比较它们的值——只有值先相等,才有资格继续往里比。
- 164 == 4,继续比 外⟷外、内⟷内值相等!把这一对标蓝(已确认对称)。但还没完——镜像要求外侧对外侧、内侧对内侧:接下来要比 4 的左孩子 ⟷ 4 的右孩子,以及 4 的右 ⟷ 4 的左。
- 17cmp: 节点7 与 节点7 是否相等?照镜子时,左半边的某个节点 7 应当正对右半边对称位置的节点 7。先比较它们的值——只有值先相等,才有资格继续往里比。
- 187 == 7,继续比 外⟷外、内⟷内值相等!把这一对标蓝(已确认对称)。但还没完——镜像要求外侧对外侧、内侧对内侧:接下来要比 7 的左孩子 ⟷ 7 的右孩子,以及 7 的右 ⟷ 7 的左。
- 19空 ⟷ 空:镜像边界情形,返回 true这对叶子 7 与 7 之下都是空(没有孩子)。镜像边界规则——两边都空就算对称成立,递归在这里触底返回 true,这条分支安全收口。
- 20cmp: 节点8 与 节点8 是否相等?照镜子时,左半边的某个节点 8 应当正对右半边对称位置的节点 8。先比较它们的值——只有值先相等,才有资格继续往里比。
- 218 == 8,继续比 外⟷外、内⟷内值相等!把这一对标蓝(已确认对称)。但还没完——镜像要求外侧对外侧、内侧对内侧:接下来要比 8 的左孩子 ⟷ 8 的右孩子,以及 8 的右 ⟷ 8 的左。
- 22空 ⟷ 空:镜像边界情形,返回 true这对叶子 8 与 8 之下都是空(没有孩子)。镜像边界规则——两边都空就算对称成立,递归在这里触底返回 true,这条分支安全收口。
- 23mirror(根左, 根右) = true所有镜像对都比完了:每一对(外侧、内侧)都相等,没有一处错位。整棵树关于根这条轴完全对称,返回 true。
- 24节点7 != 节点99 → 短路 false反例:只要把右半边一个节点从 7 改成 99,它和左半边对称位置的 7 一比就不相等(红)。镜像递归遇到第一处不等立刻短路返回 false——对称是「处处相等」,一票否决。
⚠️ 容易写错的地方
✗ 错:比 a.left 和 b.left
✓ 对:镜像要交叉:a.left ⟷ b.right
不交叉就成了「左右子树相同」而非「镜像」
✗ 错:漏判一空一有
✓ 对:一个空一个非空 → 直接 false
只比值会漏掉结构不对称
✗ 错:只比根的左右值相等就返回
✓ 对:必须递归到每一层每一对
上层对称不代表深层对称
完整代码(Java / Python / C++)
Java
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); // 内侧对内侧
}
}Python
class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
if root is None:
return True
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) # 外⟷外、内⟷内
return mirror(root.left, root.right)C++
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if (root == nullptr) return true;
return mirror(root->left, root->right);
}
private:
bool mirror(TreeNode* a, TreeNode* b) {
if (a == nullptr && b == nullptr) return true; // 都空 → 对称
if (a == nullptr || b == nullptr) return false; // 一空一有 → 不对称
if (a->val != b->val) return false; // 值不等 → 不对称
return mirror(a->left, b->right) // 外侧对外侧
&& mirror(a->right, b->left); // 内侧对内侧
}
};复杂度
时间
O(n)
每个节点最多被比较一次
空间
O(h)
递归栈深 = 树高 h;最坏(链)O(n),平衡 O(log n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 对称二叉树 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这道题为什么用「递归」,换最直接的暴力解会差在哪?+
递归抓住了本题的结构特征,把暴力解里重复的工作省掉;暴力解通常要多嵌套一层枚举,数据一大就超时。具体对比见上文「暴力解及其卡点」与「最优解逐步推演」两节。
时间复杂度为什么是 undefined?怎么推出来的?+
按上文复杂度小节的推导,时间复杂度为 undefined。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 对称二叉树 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。