📖 第42课:对称二叉树

0 阅读14分钟

想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。

📖 第42课:对称二叉树

模块:二叉树 | 难度:Easy ⭐⭐ LeetCode 链接:leetcode.cn/problems/sy… 前置知识:第40课(二叉树最大深度)、第41课(翻转二叉树) 预计学习时间:20分钟


🎯 题目描述

给定一个二叉树的根节点,判断该树是否轴对称。也就是说,树的左子树和右子树是否互为镜像。

示例:

对称树:        非对称树:
    1              1
   / \            / \
  2   2          2   2
 / \ / \          \   \
3  4 4  3          3   3

输入:root = [1,2,2,3,4,4,3]
输出:true
解释:左子树[2,3,4]和右子树[2,4,3]镜像对称

输入:root = [1,2,2,null,3,null,3]
输出:false
解释:左子树和右子树结构不对称

约束条件:

  • 树中节点数量范围是 [1, 1000]
  • -100 <= Node.val <= 100

🧪 边界用例(面试必考)

用例类型输入期望输出考察点
单节点root=[1]true基本功能
两节点对称root=[1,2,2]true简单对称
两节点不对称值root=[1,2,3]false值不同
结构不对称root=[1,2,2,3,null,null,3]false结构不同
完全对称树root=[1,2,2,3,4,4,3]true完整对称
多层不对称root=[1,2,2,null,3,3]false深层判断

💡 思路引导

生活化比喻

想象你站在一面镜子前,要判断自己是否完全对称(比如检查面部对称性)。

🐌 笨办法:用相机拍下左半边脸,水平翻转,然后逐像素比对右半边是否一样。这需要额外空间存储翻转后的图像。

🚀 聪明办法:用两个手指,一个从左眼开始,一个从右眼开始,同时向外移动,检查对应位置是否镜像相同。左手往下移一格,右手也往下移一格;左手往左移,右手就往右移。这就是双指针同步遍历的思想,无需额外空间。

关键洞察

判断对称 = 递归判断左子树的左孩子是否等于右子树的右孩子,且左子树的右孩子等于右子树的左孩子


🧠 解题思维链

这一节模拟你在面试中"从零开始思考"的过程。

Step 1:理解题目 → 锁定输入输出

  • 输入:二叉树根节点 TreeNode
  • 输出:布尔值,表示是否对称
  • 限制:节点数≤1000,递归深度安全

Step 2:先想笨办法(翻转后比较)

先翻转右子树,然后判断左子树和翻转后的右子树是否完全相同。

  • 时间复杂度:O(n) — 翻转O(n)+比较O(n)
  • 瓶颈在哪:需要先翻转,然后再遍历比较,两次遍历

Step 3:瓶颈分析 → 优化方向

其实不需要真的翻转!可以边遍历边镜像比较。

  • 核心问题:"如何同步遍历两棵子树并镜像比对?"
  • 优化思路:定义一个辅助函数,接收两个节点,递归判断它们是否镜像

Step 4:选择武器

  • 选用:递归双指针(镜像遍历)
  • 理由:用两个指针同步遍历左右子树,一个往左一个往右,天然镜像

🔑 模式识别提示:当题目涉及"对称"、"镜像",优先考虑"双指针镜像递归"


🏆 解法一:递归双指针(最优解)

思路

定义辅助函数isMirror(left, right):

  1. 两节点都为空 → 对称
  2. 一个空一个非空 → 不对称
  3. 值不同 → 不对称
  4. 递归判断:left.leftright.right对称,且left.rightright.left对称

这是双指针同步递归:一个往外,一个往内,镜像前进。

图解过程

示例1:对称树
      1
     / \
    2   2
   / \ / \
  3  4 4  3

递归调用过程:
isMirror(root.left=2, root.right=2)
├─ 值相同 ✓
├─ isMirror(2的左=3, 2的右=3)
│  └─ 值相同,都是叶子 ✓
└─ isMirror(2的右=4, 2的左=4)
   └─ 值相同,都是叶子 ✓

返回 true ✅


示例2:不对称树
      1
     / \
    2   2
     \   \
      3   3

isMirror(root.left=2, root.right=2)
├─ 值相同 ✓
├─ isMirror(2的左=null, 2的右=3)
│  └─ 一个空一个非空 ✗
返回 false ✅

Python代码

from typing import Optional


class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right


def isSymmetric(root: Optional[TreeNode]) -> bool:
    """
    解法一:递归双指针(最优解)
    思路:定义镜像判断函数,同步遍历左右子树
    """
    def isMirror(left: Optional[TreeNode], right: Optional[TreeNode]) -> bool:
        # 两个都为空,镜像
        if not left and not right:
            return True
        # 一个空一个非空,不镜像
        if not left or not right:
            return False
        # 值不同,不镜像
        if left.val != right.val:
            return False

        # 递归判断:左的左对右的右,左的右对右的左
        return (isMirror(left.left, right.right) and
                isMirror(left.right, right.left))

    # 空树是对称的
    if not root:
        return True

    # 判断左右子树是否镜像
    return isMirror(root.left, root.right)


# ✅ 测试
# 对称树:    1
#          / \
#         2   2
#        / \ / \
#       3  4 4  3
root1 = TreeNode(1)
root1.left = TreeNode(2, TreeNode(3), TreeNode(4))
root1.right = TreeNode(2, TreeNode(4), TreeNode(3))
print(isSymmetric(root1))  # 期望:True

# 不对称树:  1
#          / \
#         2   2
#          \   \
#           3   3
root2 = TreeNode(1)
root2.left = TreeNode(2, None, TreeNode(3))
root2.right = TreeNode(2, None, TreeNode(3))
print(isSymmetric(root2))  # 期望:False

# 边界测试
print(isSymmetric(TreeNode(1)))  # 期望:True(单节点)

复杂度分析

  • 时间复杂度:O(n) — 每个节点访问恰好一次
    • 具体地说:1000个节点需要约500对镜像比较
  • 空间复杂度:O(h) — 递归栈深度等于树高h
    • 平衡树:O(log n),如1000个节点约10层
    • 最坏链状树:O(n),但对称树一般较平衡

优缺点

  • ✅ 代码简洁,逻辑清晰
  • ✅ 时间O(n)最优,一次遍历
  • ✅ 空间O(h)最优,不需额外拷贝

⚡ 解法二:BFS层序遍历(迭代法)

优化思路

递归虽优雅,但有些场景要求迭代。改用队列,每次取出一对镜像节点比较,然后按镜像顺序加入下一层节点。

💡 关键想法:用队列存储成对的镜像节点,每次出队一对进行比较

图解过程

示例:  1
      / \
     2   2
    / \ / \
   3  4 4  3

BFS过程:
初始: queue=[(2, 2)]  # 左右子树根

Step1: 出队(2,2)
       值相同 ✓
       入队镜像对:(3,3), (4,4)
       queue=[(3,3), (4,4)]

Step2: 出队(3,3)
       值相同,都是叶子 ✓
       queue=[(4,4)]

Step3: 出队(4,4)
       值相同,都是叶子 ✓
       queue=[]

返回 true ✅

Python代码

from collections import deque


def isSymmetric_bfs(root: Optional[TreeNode]) -> bool:
    """
    解法二:BFS迭代
    思路:队列存储镜像节点对,逐对比较
    """
    if not root:
        return True

    # 队列存储成对的节点
    queue = deque([(root.left, root.right)])

    while queue:
        left, right = queue.popleft()

        # 都为空,继续
        if not left and not right:
            continue
        # 一个空一个非空,不对称
        if not left or not right:
            return False
        # 值不同,不对称
        if left.val != right.val:
            return False

        # 按镜像顺序加入下一层
        queue.append((left.left, right.right))  # 外侧对
        queue.append((left.right, right.left))  # 内侧对

    return True


# ✅ 测试
root1 = TreeNode(1)
root1.left = TreeNode(2, TreeNode(3), TreeNode(4))
root1.right = TreeNode(2, TreeNode(4), TreeNode(3))
print(isSymmetric_bfs(root1))  # 期望:True

root2 = TreeNode(1)
root2.left = TreeNode(2, None, TreeNode(3))
root2.right = TreeNode(2, None, TreeNode(3))
print(isSymmetric_bfs(root2))  # 期望:False

复杂度分析

  • 时间复杂度:O(n) — 每个节点入队出队各一次
  • 空间复杂度:O(w) — w为树的最大宽度,完全树最坏O(n)

🐍 Pythonic 写法

利用递归简化:

# 精简版(推荐)
def isSymmetric_compact(root: Optional[TreeNode]) -> bool:
    def mirror(t1, t2):
        if not t1 and not t2: return True
        if not t1 or not t2: return False
        return (t1.val == t2.val and
                mirror(t1.left, t2.right) and
                mirror(t1.right, t2.left))
    return mirror(root, root) if root else True

# 一行版(不推荐)
isSymmetric_oneline = lambda r: (lambda m: m(r,r) if r else True)(
    lambda l,r: not l and not r or l and r and l.val==r.val and ...)

⚠️ 面试建议:先写清晰版本展示思路,通过后可以说"可以简化",但不要影响可读性。


📊 解法对比

维度🏆 解法一:递归双指针解法二:BFS迭代
时间复杂度O(n) ← 最优O(n)
空间复杂度O(h) 约O(log n)O(w) 约O(n)
代码难度简单中等
面试推荐⭐⭐⭐ ← 首选⭐⭐
适用场景通用,最直观避免递归要求
代码行数约15行约20行

为什么解法一是最优解:

  • 时间O(n)已是理论最优(必须访问所有节点对)
  • 空间O(log n)优于BFS的O(n)
  • 双指针镜像递归思想清晰,面试中易讲解
  • 代码简洁,易于理解和实现

面试建议:

  1. 直接说出🏆最优解思路:"定义镜像判断函数,左的左对右的右,左的右对右的左"
  2. 写代码时强调递归三个出口:"都空、一空、值不同"
  3. 手动模拟一个对称树和非对称树的判断过程
  4. 主动测试边界:"单节点返回true,空树返回true"
  5. 如被问"能否迭代?",给出解法二BFS方案

🎤 面试现场

模拟面试中的完整对话流程,帮你练习"边想边说"。

面试官:请你判断一棵二叉树是否对称。

:(审题15秒)好的,这道题要求判断树是否轴对称,也就是左右子树互为镜像。

我的思路是用递归双指针。定义一个辅助函数isMirror(left, right),同时遍历左右子树,判断它们是否镜像。关键是镜像的定义:左的左对右的右,左的右对右的左。时间复杂度O(n),空间复杂度O(h)。

面试官:很好,请写代码。

:(边写边说)

def isSymmetric(root):
    def isMirror(left, right):
        # 三个递归出口
        if not left and not right:
            return True
        if not left or not right:
            return False
        if left.val != right.val:
            return False
        # 递归判断镜像
        return (isMirror(left.left, right.right) and
                isMirror(left.right, right.left))

    if not root:
        return True
    return isMirror(root.left, root.right)

面试官:为什么要用两个参数的辅助函数?

:因为对称判断需要同时遍历两棵子树,而单个递归函数只能处理一个节点。辅助函数接收两个节点,可以同步递归,一个往左一个往右,实现镜像比对。这是"双指针同步递归"的思想。

面试官:测试一下?

:用示例树[1,2,2,3,4,4,3]...

  • isMirror(左2, 右2):值相同
    • isMirror(3, 3):都是叶子,返回true
    • isMirror(4, 4):都是叶子,返回true
    • 都返回true,所以根节点对称 ✅

再测不对称的[1,2,2,null,3,null,3]:

  • isMirror(左2, 右2):值相同
    • isMirror(左2的左null, 右2的右3):一空一非空,返回false ✗

边界:单节点树,左右都是null,返回true ✅

面试官:不错!

高频追问

追问应答策略
"能否不用辅助函数?""可以用栈或队列迭代,但辅助函数最清晰,分离了'判断对称'和'判断镜像'两个逻辑"
"如果允许翻转节点呢?""那就变成'翻转等价'问题,需要额外判断不翻转的情况,复杂度变O(n²)"
"空间能O(1)吗?""不能,必须用栈(显式或递归)来同步遍历两个子树,无法纯迭代实现"
"这题和翻转二叉树什么关系?""可以先翻转右子树,再判断左右是否相同,但那样需要两次遍历,不如双指针一次完成"

🎓 知识点总结

Python技巧卡片 🐍

# 技巧1:多条件短路判断 — and会短路,遇False立即停止
if not left and not right:  # 都空才继续
if not left or not right:   # 任意一个空就返回

# 技巧2:嵌套函数访问外层变量 — 闭包特性
def outer(root):
    def inner(node):
        return node.val  # 可访问root
    return inner(root)

# 技巧3:递归返回布尔值的优雅写法
return (cond1 and cond2 and cond3)  # 多条件同时满足

💡 底层原理(选读)

为什么对称判断需要"双指针"?

对称性的本质是同步比对两个镜像位置。单指针只能遍历一个子树,无法知道镜像位置的节点是什么。

类比:判断字符串是否回文,为什么用双指针?

def is_palindrome(s):
    left, right = 0, len(s) - 1
    while left < right:
        if s[left] != s[right]:
            return False
        left += 1
        right -= 1
    return True

树的对称判断是这个思想的递归版本:左指针往外走,右指针往内走,镜像前进。

递归vs迭代:谁的空间更优?

  • 递归:栈深度=树高h,对称树通常较平衡,约O(log n)
  • 迭代(BFS):队列宽度=树的最大宽度w,完全树约O(n/2)
  • 所以对于对称树,递归通常空间更优!

但如果树极度偏斜(不对称),递归可能O(n),迭代可能O(1),这时迭代更优。

算法模式卡片 📐

  • 模式名称:双指针镜像递归
  • 适用条件:需要同步比对树的镜像位置
  • 识别关键词:"对称"、"镜像"、"回文"
  • 模板代码:
def check_symmetric(root):
    def mirror(left, right):
        # 递归出口:都空/一空/值不同
        if not left and not right:
            return True
        if not left or not right:
            return False
        if left.val != right.val:
            return False
        # 镜像递归:外对外,内对内
        return (mirror(left.left, right.right) and
                mirror(left.right, right.left))
    return mirror(root.left, root.right)

易错点 ⚠️

  1. 递归顺序错误

    • ❌ 错误:mirror(left.left, right.left) 不是镜像,是同侧
    • ✅ 正确:mirror(left.left, right.right) 外侧对外侧
  2. 忘记处理空节点

    • ❌ 错误:只判断if left.val != right.val,在None上调用.val会报错
    • ✅ 正确:先判断是否为空,再比较值
  3. 单节点返回值错误

    • ❌ 错误:认为单节点不对称返回false
    • ✅ 正确:单节点左右都是null,镜像,返回true

🏗️ 工程实战(选读)

这个算法思想在真实项目中的应用,让你知道"学了有什么用"。

  • 场景1:图形对称性检测

    # Photoshop的"检查对称性"工具
    # 用于设计Logo时验证左右对称
    # 底层就是像素块树的镜像递归
    
  • 场景2:分子对称性分析

    # 化学软件判断分子是否有对称轴
    # 决定分子的旋光性(光学活性)
    # 原子树的镜像判断
    
  • 场景3:代码语法树对称检测

    # 某些编程语言的语法糖检测
    # 如Lisp的括号对称性验证
    # (a (b c) (c b)) 对称
    

🏋️ 举一反三

完成本课后,试试这些同类题目来巩固知识:

题目难度相关知识点提示
LeetCode 100. 相同的树Easy双指针递归不需要镜像,直接左对左右对右
LeetCode 572. 另一棵树的子树Easy递归+遍历先遍历找根,再判断子树是否相同
LeetCode 951. 翻转等价二叉树Medium镜像递归允许翻转,需判断两种情况
LeetCode 1490. 克隆N叉树Medium递归复制类似思想:递归克隆子树

📝 课后小测

试试这道变体题,不要看答案,自己先想5分钟!

题目:给定一棵二叉树,判断它是否垂直对称(即以根节点为轴,上下对称,而非左右)。

💡 提示(实在想不出来再点开)

垂直对称等价于:按层遍历,每一层是回文数组。可以用BFS层序遍历,每层检查是否回文。

✅ 参考答案
from collections import deque

def isVerticalSymmetric(root: Optional[TreeNode]) -> bool:
    """
    判断树是否垂直对称(上下对称)
    思路:BFS层序遍历,每层检查是否回文
    """
    if not root:
        return True

    queue = deque([root])

    while queue:
        level_vals = []
        level_size = len(queue)

        for _ in range(level_size):
            node = queue.popleft()
            if node:
                level_vals.append(node.val)
                queue.append(node.left)
                queue.append(node.right)
            else:
                level_vals.append(None)  # 占位符

        # 检查当前层是否回文
        if level_vals != level_vals[::-1]:
            return False

    return True

核心思路:

  • BFS遍历每一层,包括None占位符
  • 每层用双指针检查回文
  • 时间O(n),空间O(w)

注意:这题在LeetCode上没有,是自创变体,用于练习BFS+回文判断。


💬 知识拓展

对称性的数学本质:

  • 轴对称(本题):沿中轴翻转后与自己重合
  • 中心对称:绕中心旋转180°后重合
  • 旋转对称:旋转某个角度后重合

在树的语境中:

  • 轴对称:左右子树镜像
  • 中心对称:不存在(树没有旋转180°的概念)
  • 旋转对称:N叉树可能有(如完全三叉树旋转120°)

这道题训练的是递归思维+镜像遍历,是理解树的对称性的最佳入门题!


如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。