对称二叉树 图解题解
不用造镜像树——让递归同时握住左右两个节点交叉核对就行。
对称就像两个人站在镜子两侧做同一套动作:左边举左手、右边必须举右手,才算真正镜像。不需要先复制出一个镜像人再逐动作比,只要派一个裁判同时盯着两人——左的左和右的右是外侧一对、左的右和右的左是内侧一对,两对都一致才算这一层通过,然后裁判继续往下一层走。
这道题到底在问什么
- 树
- 1 →(2,2),左2→(3,4),右2→(4,3)
- 输出
- true
先想最直接的笨办法
最直觉的想法:先复制整棵树、把每个节点的左右孩子翻转造出「镜像树」,再和原树一个一个比。能对,但要额外建一棵树、多走一遍,空间和代码都更重,纯属多此一举。(动画第 3 步)
最优解:一步一步想明白
- 3最直觉的想法:先复制整棵树、把每个节点的左右孩子翻转造出「镜像树」,再和原树一个一个比。能对,但要额外建一棵树、多走一遍,空间和代码都更重,纯属多此一举。
- 4转折:对称的本质是「左子树」和「右子树」互为镜像,不必真造出镜像树,让一个递归同时握住左 L、右 R 两个节点对照走即可。规则:都空就对称;一空一非就不对称;否则要求
L.val 等于 R.val,且 L 的左配 R 的右(外侧)、L 的右配 R 的左(内侧)。这题能这么做,是因为对称是「两两镜像位置」的局部判断,天然适合双节点同步递归。 - 5isMirror(左2, 右2)根 1 自己不用比,直接拿它的左孩子 L 等于 2 和右孩子 R 等于 2 开镜像比较。两个都非空,先看值:
2 == 2通过。转折点:不去真造镜像树,而是就让这一个递归同时握住 L、R 往下走。 - 6展开 L,R 的孩子L 等于 2、R 等于 2 值相等后,对称要求交叉地往下递归两对:外侧 是 L.left 配 R.right、内侧 是 L.right 配 R.left。这两对都成立,整支才算镜像。先看外侧。
- 7isMirror(3, 3)第一对外侧:左孩子的左 (3) 配 右孩子的右 (3)——这是离中线最远的一对。注意是交叉取:左的 left 对右的 right。
3 == 3通过,且两边都是叶子,这一对镜像成立。 - 8isMirror(4, 4)第二对内侧:左孩子的右 (4) 配 右孩子的左 (4)——靠近中线的一对,同样交叉取。
4 == 4通过。外侧、内侧两对都成立,左 2 和右 2 这一支镜像完全通过。 - 9外✓ 内✓左 2 与右 2 这一支:值相等、外侧相等、内侧相等,三项都成立,
isMirror(左2, 右2)返回 True。这棵小树只有这一支要比,根的两个孩子镜像即整树镜像。 - 10值 → 外侧 → 内侧把这一支慢放一遍,记住每次
isMirror永远是三连:先比手里这两个节点的值(高亮的中层 2 和 2),再交叉连外侧那对(最外的 3 和 3),再交叉连内侧那对(中间的 4 和 4)。交叉就是「左的外 配 右的外」——连线总在中线两边镜像着走。 - 11全部 ✓ → return True每一对镜像位置都相等,递归全程返回 True,整棵树对称。只要途中有一对不等或结构对不上,就会立刻短路返回 False。下面看看那种不对称的情况长什么样。
- 12isMirror(9, 4)假设左孩子的右改成了 9。走到内侧这对:L.right=9 配 R.left=4,
9 != 4不成立。这一对镜像失败。注意外侧的 3 配 3 其实已经通过了,问题只出在内侧这一对。 - 13不对称 → return False只要任意一对镜像位置不相等,不必再比其余节点,直接 return False——递归靠
and短路,一处假则全假。回到正例:正因为每一对都相等,才返回 True。 - 16判断对称、判断两树相同、合并两棵树,都用「同时握住两个节点往下递归」的双指针式 DFS。对称就交叉(左配右)、相同就平行(左配左)——只差递归传参那一处。
- 21把这题练到能复述后,去 二叉树专题 里迁移:交叉换平行就是 LC100 相同的树、再套一层枚举子树就是 LC572 另一棵树的子树、把比较换成「拷贝/翻转」就是 LC226 翻转二叉树。卡壳就点右下角问小欧,或去通关训练里连刷这一串。
⚠️ 容易写错的地方
✗ 错:对称写成平行 same(L.left, R.left)
✓ 对:对称要交叉 same(L.left, R.right)
镜像是外侧配外侧、内侧配内侧,平行比就成了判「两边长得一样」而非「镜像」,正好做成了 LC100
✗ 错:只比值不判空,遇 None 取 .val 报错
✓ 对:先判空:都空就真、一空一非就假
结构不同(一边有孩子一边没有)也算不对称,且不先挡住 None 会崩
✗ 错:入口直接 same(root.left, root.right) 没挡空根
✓ 对:先 if not root: return True 再进 same
root 为空时 root.left 直接抛异常;C++/Java 用 !root || ... 短路也是同一道保险
完整代码(Python / C++ / Java)
Python
class Solution:
def isSymmetric(self, root):
if not root:
return True
def same(a, b):
if not a and not b:
return True
if not a or not b:
return False
return a.val == b.val and same(a.left, b.right) and same(a.right, b.left)
return same(root.left, root.right)C++
class Solution {
bool same(TreeNode* a, TreeNode* b) {
if (!a && !b) return true;
if (!a || !b) return false;
return a->val == b->val && same(a->left, b->right) && same(a->right, b->left);
}
public:
bool isSymmetric(TreeNode* root) { return !root || same(root->left, root->right); }
};Java
class Solution {
boolean same(TreeNode a, TreeNode b) {
if (a == null && b == null) return true;
if (a == null || b == null) return false;
return a.val == b.val && same(a.left, b.right) && same(a.right, b.left);
}
public boolean isSymmetric(TreeNode root) { return root == null || same(root.left, root.right); }
}套路模板 · 双树同步递归骨架(挖空)
def dfs(a, b):
if not a and not b: return True # 1) 两个都空 → 这对到底了,算过
if not a or not b: return False # 2) 一空一非 → 结构都不一致,直接否
if a.val != b.val: return False # 3) 值不等 → 否
# 4) 递归两对子节点 —— 只有这一行决定你在做哪道题:
return dfs(a.?, b.?) and dfs(a.?, b.?)
# 对称(LC101): dfs(a.left, b.right) and dfs(a.right, b.left) ← 交叉
# 相同(LC100): dfs(a.left, b.left) and dfs(a.right, b.right) ← 平行bool dfs(TreeNode* a, TreeNode* b) {
if (!a && !b) return true; // 两个都空
if (!a || !b) return false; // 一空一非
if (a->val != b->val) return false;
// 这一行决定题型:
return dfs(a->?, b->?) && dfs(a->?, b->?);
// 对称: a->left,b->right && a->right,b->left (交叉)
// 相同: a->left,b->left && a->right,b->right (平行)
}boolean dfs(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 dfs(a.?, b.?) && dfs(a.?, b.?);
// 对称: a.left,b.right && a.right,b.left (交叉)
// 相同: a.left,b.left && a.right,b.right (平行)
}复杂度
时间复杂度
O(n)
每对镜像节点比较一次,共约 n 个节点
空间复杂度
O(h)
递归栈深度等于树高 h;最坏退化成链时为 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 对称二叉树 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么是「外侧配外侧、内侧配内侧」交叉比较?+
镜像对称意味着:左子树最外侧要等于右子树最外侧(L.left vs R.right),最内侧也要相等(L.right vs R.left)。沿中线对折,外侧压外侧、内侧压内侧才重合。
和「相同的树 LC100」差在哪?+
LC100 是平行比较 a.left 配 b.left;对称是交叉比较 a.left 配 b.right。判空判值的骨架完全相同,只差最后递归传参那一行。
能用迭代吗?+
能。用队列成对入队:先把 (root.left, root.right) 放进去,每次取一对比较,再把 (L.left,R.right) 和 (L.right,R.left) 两对入队,队空且全程相等就对称。本质和递归一样,只是手动维护栈/队列。
递归会栈溢出吗?复杂度多少?+
时间 O(n) 每个节点访问一次;空间 O(h) 栈深等于树高,平衡树约 log n,退化成链时 O(n)。极深的树才有溢出风险,那时改用迭代队列版更稳。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。