news 2026/9/16 3:40:17

力扣101:对称二叉树的递归与迭代解法详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
力扣101:对称二叉树的递归与迭代解法详解

不用引入太多背景,直接说结论:力扣第101题“对称二叉树”是一道非常典型的二叉树递归/迭代练习题,也是面试里出现频率很高的基础题。很多人在刚接触二叉树时,被遍历、深度、翻转这些概念绕晕,等做到“对称”这道题时又开始混淆“左右子树相等”和“镜像对称”的区别。这篇文章我直接把这道题从头拆到尾,从题目本质、递归解法的思考方式、迭代解法的队列写法,到容易踩的坑和可以延伸的知识点,一次性讲透。

先明确我要解决的问题:给定一棵二叉树的根节点root,检查它是否轴对称。所谓轴对称,就是这棵树从中间画一条竖线,左右两侧是镜像关系。注意,不是左子树和右子树完全相同,而是左子树的左孩子要对应右子树的右孩子,左子树的右孩子要对应右子树的左孩子。这个对应关系是整道题的灵魂。

1. 题目拆解:对称的本质是“镜像比较”

1.1 从一棵树到两棵树的转化

第一次看到这道题,很多人的第一反应是:我能不能把左子树翻转一下,然后和右子树比较?这是一个很自然的思路,但实际操作起来会多出额外步骤,比如要先写一个翻转树的函数,然后再写一个判断两棵树是否相同的函数,两个函数叠在一起,逻辑上绕了一圈。

更直接的思路是:既然对称是镜像关系,那我不需要真的翻转,而是同时遍历左子树和右子树,每次比较两个对应位置的节点。也就是说,把“判断一棵树是否对称”拆成“判断两棵树是否镜像对称”。

这个转化非常关键。它把一个看似只涉及单棵树的问题,变成了双树比较问题。很多二叉树的题目都是这个套路,比如判断两棵树是否相同,也是同时遍历两棵树。对称只是在这个基础上,把左右孩子的访问顺序换了一下。

1.2 镜像比较的三个条件

对于两棵子树pq,它俩互为镜像,必须同时满足三个条件:

  • p.val == q.val,也就是当前节点的值相等。
  • p.leftq.right互为镜像,即 p 的左孩子要对应 q 的右孩子。
  • p.rightq.left互为镜像,即 p 的右孩子要对应 q 的左孩子。

为什么是交叉对应?因为镜像嘛。你对着镜子举左手,镜子里的人举的是右手。所以左对右,右对左,这就是镜像的核心。

有了这三个条件,写递归就水到渠成了。递归函数接收两个节点,先判断值,再递归判断它们的孩子。终止条件是:两个节点都为空,返回 true;一个为空一个不为空,返回 false。

1.3 力扣原题给出的小陷阱

原题给的例子很简单:

1 / \ 2 2 / \ / \ 3 4 4 3

这个是对称的。但很多人看例子会误以为“只要左右孩子值相等就行”,于是写出了只比较root.left.val == root.right.val就返回 true 的代码。这在大数据量下一定错。

还有一种常见误解,是把对称理解为“左子树的先序遍历结果等于右子树的后序遍历结果”。这个思路在特定条件下能工作,但需要考虑空节点的表示,而且实现起来不如直接递归直观。我建议初学者先掌握递归和迭代两种标准解法,再考虑这些花式技巧。

2. 递归解法:短路思想与边界条件的处理

2.1 核心代码与逐行解释

递归解法是这道题最简洁的写法。我用 Python 写了一遍,结构非常清晰:

class Solution: def isSymmetric(self, root: Optional[TreeNode]) -> bool: if not root: return True def check(p: Optional[TreeNode], q: Optional[TreeNode]) -> bool: if not p and not q: return True if not p or not q: return False if p.val != q.val: return False return check(p.left, q.right) and check(p.right, q.left) return check(root.left, root.right)

这段代码里有几个细节值得展开讲:

第一个细节是if not root: return True。空树算不算对称?在力扣的定义里,空树是对称的。这符合数学上“空集满足任意性质”的约定,在递归时也天然成立,因为根节点为空时,左右子树都不存在。

第二个细节是把递归函数定义在isSymmetric内部。这样可以方便地访问根节点的左右孩子,也避免了额外传参。当然你也可以定义成外部函数,传入root.leftroot.right,效果一样。

第三个细节是递归的终止条件顺序。先判断两个都空,再判断一个空,最后才比较值。这个顺序不能乱。如果先比较值,空节点会直接报错。在 Python 里对None.val会抛AttributeError,所以在任何递归里,空节点判断都要放在最前面。

第四个细节是check(p.left, q.right) and check(p.right, q.left)这个表达式。Python 的and是短路求值的,也就是说,如果左边返回 False,右边根本不会执行。这在递归里天然形成剪枝,一旦发现不对称,立刻终止递归,不会白白遍历整棵树。

2.2 递归过程的手动模拟

为了彻底理解递归的执行过程,我拿上面那个对称的例子手动走一遍。

根节点是 1,进入check(root.left, root.right),也就是check(节点2, 节点2)

第一步,两个节点都不为空,值都是 2,相等。

第二步,递归调用check(节点2.left, 节点2.right),也就是check(节点3, 节点4)。两个节点值分别为 3 和 4,不相等,返回 False。

等等,这个例子里节点3和4都是叶子节点,正常情况下左边是3右边是4,不对称吗?注意,我这里用的是力扣例子里的结构:

1 / \ 2 2 / \ / \ 3 4 4 3

根节点 1 的左孩子的左孩子是 3,根节点 1 的右孩子的右孩子是 3。所以在check(root.left, root.right)里,p.left是左侧的 3,q.right是右侧的 3,两个值相等。而p.right是左侧的 4,q.left是右侧的 4,两个值也相等。只有当树变成:

1 / \ 2 2 / \ / \ 3 4 3 4

这种情况下,左侧的 3 对应右侧的左孩子 3,但镜像对应的是右侧的右孩子 4,才会立刻发现不对称。

我在实际模拟时经常把指向搞混,后来总结了一个口诀:递归时只关心“当前这一层传入的两个节点”,剩下的交给下一层。只要记住了左对右、右对左,递归的调用关系就不会错。

2.3 递归的时间与空间复杂度分析

时间复杂度是 O(n),n 是二叉树的节点数。因为每个节点最多被访问一次,最坏情况下(对称时),递归会遍历整棵树的所有节点。如果不对称,可能会提前终止,但最坏复杂度仍然是 O(n)。

空间复杂度是 O(n) 的额外空间,因为递归调用栈的深度取决于树的高度。最坏情况是树退化成链表,高度为 n,递归栈会压入 n 层。最好的情况是平衡树,高度为 log(n),空间复杂度就是 O(log n)。面试时如果被问到空间复杂度,要把这两种情况都说清楚,只答 O(n) 或 O(log n) 都不完整,考官想听的是你理解树高度对递归栈的影响。

3. 迭代解法:队列实现层序式成对校验

3.1 为什么递归不够,还要学迭代

递归解法虽然简洁,但也有天然短板。当树的深度很大时,递归调用栈可能会溢出,这是函数调用机制决定的。比如树退化成一条链,深度达到几万层,Python 默认的递归深度限制是 1000 左右,直接 RecursionError。

迭代解法则没有这个顾虑,它用显式的队列或栈来模拟递归过程,不依赖系统调用栈。这也是面试官喜欢追问的第二问:“能不能不用递归实现?”如果只会递归写法,面试印象分会打折扣。

3.2 队列模拟的核心思路

迭代解法的思路是:把需要比较的两个节点成对放入队列,每次从队首取出两个节点进行比较,然后把它们的镜像子节点成对入队。

具体步骤如下:

  1. 初始化队列,将root.leftroot.right成对入队。
  2. 从队列中取出两个节点uv
  3. 如果uv都为空,跳过本轮循环。
  4. 如果uv只有一个为空,返回 False。
  5. 如果u.val != v.val,返回 False。
  6. u.leftv.right成对入队,再将u.rightv.left成对入队。
  7. 循环直到队列为空,返回 True。

注意入队的顺序必须是两两一组,不能只入队一个节点。我用 Python 实现时习惯用collections.deque,因为列表的pop(0)是 O(n) 的,而dequepopleft()是 O(1) 的。

from collections import deque class Solution: def isSymmetric(self, root: Optional[TreeNode]) -> bool: if not root: return True queue = deque([root.left, root.right]) while queue: u = queue.popleft() v = queue.popleft() if not u and not v: continue if not u or not v: return False if u.val != v.val: return False queue.append(u.left) queue.append(v.right) queue.append(u.right) queue.append(v.left) return True

这段代码里最需要注意的是第六步的入队顺序。u.left必须和v.right配对,u.right必须和v.left配对,这是镜像的核心。如果写成了u.leftv.left配对,那就是在判断两棵树是否相等,而不是是否镜像。这个错误非常隐蔽,尤其在队列很长时很难一眼看出来,我自己就曾经在这种地方排查了很久。

3.3 用栈替代队列:殊途同归

队列解法用到了先进先出的特性,但实际上用栈也能实现,只要保持成对弹入弹出的顺序不变即可。如果你把上面的queue换成普通的 list,用appendpop()模拟栈,效果完全一样。

这是因为我们只关心“成对比较”这个约束,不关心比较的先后顺序。无论是 BFS 式的层序比较,还是 DFS 式的深度优先比较,只要每对节点是镜像对应的,最终结果都一样。

在面试时主动提一句“队列和栈都可以,因为比较顺序不影响结果”,会显得你对问题的理解更深一层。这也是从“会做题”到“懂原理”的一个小分水岭。

3.4 迭代解法的复杂度对比

迭代解法的时空复杂度都是 O(n)。时间上每个节点入队出队一次;空间上队列最多同时存储两层的节点,最坏情况下(完全二叉树)最后一层有约 n/2 个节点,所以空间复杂度也是 O(n)。

和递归相比,迭代的优势在于没有调用栈溢出的风险。劣势是代码相对啰嗦,而且容易在入队顺序上写错。我觉得两者没有绝对的优劣,关键是理解各自的适用场景:面试时首选递归,写起来快;实际工程里如果树可能很深,用迭代更稳。

4. 常见误判与调试技巧实录

4.1 误区一:直接比较左右子树先序遍历序列

想过用序列化来偷懒的朋友应该不少。思路是:把左子树做一次先序遍历,把右子树做一次后序遍历,如果序列相同就对称。

这个思路理论上说得通,但实现时有个大坑:空节点怎么表示。如果不用特殊字符标记空节点,光靠节点值序列,很多树会得到相同的序列。比如下面的两棵树:

1 1 / \ / \ 2 2 2 2 / \ 3 3

它们的节点值序列都是 1, 2, 3, 2 或 1, 2, 2, 3,单靠值序列根本区分不开。所以如果非要用序列化,一定要用例如'#'来代表空节点,并且保证左右子树的遍历方向相反。这个问题在讨论“对称树”这类问题时很容易被忽略,但实际写代码时一定会踩坑。

4.2 误区二:只做层序遍历并检查每层回文

另一个常见的思路是:用层序遍历收集每一层的节点值,然后检查每一层是不是回文数组。

这个思路能通过一些测试用例,但会在一种情况下出问题:空节点的位置没有被保留。比如下面的树:

1 / \ 2 2 \ \ 3 3

第一层是 [1],第二层是 [2, 2],第三层如果只收集非空值是 [3, 3],看起来是回文,但这棵树实际上不是对称的,因为左边 3 是右孩子,右边 3 也是右孩子,位置不对应。

解决办法是层序遍历时保留空节点的占位符,比如用None表示空,然后再检查回文。这样可行,但实现起来比迭代解法更繁琐,而且队列里可能塞入大量空节点,空间利用率不高。我觉得作为思路拓展可以聊一聊,但不建议作为面试时的首选解法。

4.3 实际调试中我常用的三个小技巧

技巧一是多用“不对称”的例子来验证。比如只修改树中一个节点的值,确保代码能检测出来。很多人测试时只拿对称的例子跑,一遍通过就以为没问题,结果换了个用例就挂。我习惯准备至少三个用例:对称树、非对称树、空树。

技巧二是打印递归的调用对。在递归函数入口加一行print(p.val if p else None, q.val if q else None),可以看到每层比较的是哪两个节点。这一步对排查镜像对应关系极有帮助。

技巧三是在迭代解法中,不要把成对的节点塞进队列后忘了校验队列长度。因为每次循环取出两个节点,如果入队时不小心只塞了一个,队列长度变成奇数,就会在下一次popleft()时抛异常。比较好的习惯是每次入队都成对append,循环体内先判断len(queue) >= 2再做操作。

5. 从对称二叉树延伸:二叉树的深度、遍历与搜索树的交叉联想

5.1 对称与遍历:不改变不破坏原有结构

学完对称二叉树,再回头看热搜词里的“二叉树的遍历”,你会发现它们其实是同一套思维体系。遍历的核心是“访问顺序”,对称的核心是“比较顺序”。如果你能把二叉树的先序、中序、后序、层序遍历都写熟练,对称题里的递归和队列写法就只是换了个用途而已。

比如迭代解法中我用队列成对取节点,本质就是层序遍历的变形。层序遍历每次取一个节点并访问它的左右孩子,对称迭代每次取两个节点并比较它们的镜像子节点。如果你对层序遍历的模板足够熟悉,这个变形不需要死记硬背,现场推就能推出来。

“二叉树的深度”这个热搜词也和对称有关联。对称树不一定平衡,但平衡树大概率看起来更对称。深度本身不直接参与对称判断,但递归解法的空间复杂度来自深度,所以理解深度对分析递归栈有帮助。

5.2 对称与搜索二叉树:验证顺序的另一个方向

搜索二叉树(BST)定义的是节点值的排序关系:左孩子小于根,右孩子大于根。对称二叉树定义的是结构关系,两者不冲突但也不等价。一棵 BST 完全可能不对称,比如:

5 / \ 3 8 / \ 1 4

按 BST 规则是合法的,但左右子树并不镜像对称。

如果把这两类问题放在一起复习,你会发现二叉树的题目基本都是三板斧:递归函数的设计、遍历顺序的选择、边界条件的处理。对称二叉树同时用到了递归和层序迭代,是一道很好的综合训练题。

5.3 变体题:怎么判断一棵树的子树是否对称

面试官经常会在原题基础上加问一句:“如果给你一个根节点,如何找到这棵树中最大的对称子树?”

这个问题的思路是:遍历每个节点,以该节点为根判断是否对称,同时记录最大深度。判断函数可以直接复用原题的isSymmetric,整体时间复杂度是 O(n^2)。如果追求更优解,可以用后序遍历加上子树哈希的手段来降低重复计算,但这是比较进阶的玩法,初学者先掌握 O(n^2) 的暴力解法即可。

这类变体题的价值在于:它逼着你把一个函数抽象成可复用的工具函数。原题里check(p, q)比较的是两个节点,放到变体里就变成了一个独立的判断逻辑,被反复调用。很多人拿到变体题就懵,本质上是没有意识到“原题已经被我写成了一个完整的函数”。

6. 实际面试中怎么回答这道题才能加分

6.1 从“背题”到“讲题”

力扣上有不少人是靠背答案过题的,但面试官早就免疫了。你光背出递归代码,他马上追问迭代写法;你写出迭代,他又问复杂度;你答出复杂度,他还能问“如果树非常大,递归栈会怎样”。这也是我反复强调要理解原理的原因。

我建议的回答顺序是:先说对称的定义(左对右、右对左),再说递归解法(三个终止条件加一个递归式),接着补充迭代解法(队列成对比较),最后主动分析复杂度。每一步都用一句人话解释“为什么”,面试官会明显感受到你是真懂。

6.2 一道题背后的知识网络

这道题虽然简单,但牵扯出来的知识点其实覆盖了二叉树的大部分基础:递归思想的落地、队列的运用、树的深度、空节点的处理、复杂度分析。如果你刚刚开始刷二叉树,把这道题吃透,再去做“相同的树”“翻转二叉树”“二叉树的最大深度”,会顺畅许多,因为它们共用同一套思维模型。

就我自己的刷题体验来说,最容易提升效率的方法不是刷题数量,而是每做完一道题,花十分钟想一想:这道题和之前哪道题相似?代码的骨架能不能抽出来?换一个条件会变成什么题?“对称二叉树”就是一个特别适合做这种思考的样本,因为它简单到不吓人,又复杂到足够承载递归、迭代、边界处理和复杂度分析四块内容。

7. 最后的实操建议与个人体会

我刷这道题的时候,最深的体会是:递归代码写得快,不代表理解到位。评判标准是你能不能在没有提示的前提下,把递归改成迭代。改完之后,再试试用层序遍历回文检测法做一遍,虽然不推荐面试使用,但能帮你深刻理解空节点占位的重要性。

做这道题时还建议准备几个容易出错的用例,包括只有根节点、节点值都为负数、左右子树高度差很大等情况。把这些用例跑通,基本上代码的鲁棒性就过关了。我个人习惯在做完题后额外写一个“临时修改某个节点值”的小函数,用来快速生成不对称的测试数据,这样就不用每次手动构造一棵新树了。

最后再分享一个调试小细节:如果你用的是 Python,递归函数里要避免直接使用全局变量来传参,尽量把需要比较的数据都放在函数参数里,否则在多层递归时容易出现状态污染。队列迭代也一样,不要在一个 while 循环里改动其他无关变量,保持每个循环周期独立。把这类工程习惯带到刷题里,时间长了你会发现,自己的代码质量和调 bug 速度都会上一个台阶。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 3:37:46

UVa 11509 Touring Robot:圆缩点与BFS网格搜索的几何避障解法

在做算法竞赛题目的时候,我最大的感受是:很多所谓“难”的题,其实不是代码量大,也不是某个算法特别复杂,而是题目本身披了一层“故事外衣”,你得把那层外衣剥掉之后,才能看到里面真正要求的东西…

作者头像 李华
网站建设 2026/9/16 3:37:39

基于DRO与CVaR的电力市场发电商自调度优化与MATLAB实现

电力市场里做日前自调度,最难的不是机组组合那套整数变量,而是电价到底怎么建模。你拿着历史场景做随机规划,第二天来个尖峰价格,利润直接被打回原形;改用区间鲁棒优化,又把最乐观的情况全丢掉,…

作者头像 李华
网站建设 2026/9/16 3:37:26

ASIL D认证RTOS与Microkernel:车规级功能安全的底层基石

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 3:37:10

星核精退场?从星核到开拓命途:星穹铁道4.5版本剧情猜想

昨晚临睡前刷社区,手指一顿,一条帖子标题把我钉在原地——《世间再无星核精!开拓者接下来莫非要横扫六合?》【星穹铁道开拓者/星穹/4.5版本剧情】。说真的,看到"星核精"三个字我大腿都…

作者头像 李华
网站建设 2026/9/16 3:36:04

轻量级Markdown编辑器Markpad:打开即写,告别笨重全家桶

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 3:34:44

Linux 6.12内核负载均衡触发机制详解:从tick到nohz

压测的时候经常遇到一个奇怪现象:明明整台机器好几个核都闲着,任务却死死堆在其中一个核上,好一会儿才被摊匀。很多人第一反应是“负载均衡没生效”,其实内核的负载均衡根本不是后台线程一直在扫描,它是散落在几个特定…

作者头像 李华