在二叉树相关算法题中,对称性判断是一道经典且高频的题目。它不仅考察对树结构的理解,更考验递归与迭代两种核心编程思想的灵活运用。本文将深入剖析 LeetCode 101 题,带你从思路推导到代码落地,彻底吃透对称二叉树问题。无论你是准备面试,还是夯实基础,这篇文章都值得一看。
为什么对称二叉树值得深入思考?
对称二叉树,简单来说就是一棵树关于根节点左右镜像对称。判断对称性,不能只比较左右子节点的值是否相等,因为对称要求的是左子树的左子树与右子树的右子树相等,左子树的右子树与右子树的左子树相等。这种嵌套的对应关系,恰好是递归最擅长的场景。
同时,它也是理解迭代遍历、队列应用的好素材。许多算法题(如镜像树、翻转树)都与其有异曲同工之妙。掌握本题的两种解法,能帮你构建更系统的树形问题解决框架。
一、题目回顾与初步思路
题目要求判断给定的一棵二叉树是否对称。示例中,对称树返回 true,非对称树返回 false。题目保证了至少有一个节点,因此无需处理空树。
拿到题目,常见的初步想法有三种:
- 层序遍历思路:逐层检查每层节点是否对称。但实现稍显繁琐,且需要额外记录空位。
- 翻转比较法:先将左子树翻转成右子树的镜像,再与右子树逐节点比较。思路直观,但效率一般。
- 递归比较法:直接递归比较对称位置的节点。这是最优雅的方案,也是本文重点。
如果已经做过“翻转二叉树”这道题,你可能会想:把左子树翻转后,和右子树比较不就行了?理论上可行,但递归解法更符合树的天然结构,也更高效。下面我们详细展开递归解法。
二、递归解法:三步走,思路清晰
递归的难点在于如何设计递归函数。这里推荐按照递归三要素来思考:
1. 确定参数和返回值
对于对称判断,我们不需要额外的数组或计数器,只需要比较两个节点是否对称。因此,递归函数的参数应该是两个节点(左子树的当前节点和右子树的当前节点),返回值是布尔类型 true 或 false。
2. 确定终止条件
终止条件是递归的边界,这里需要分情况讨论:
- ✅ 当两个节点都为空(即越过叶子),说明对称,返回 true。
- ⚠️ 当其中一个为空,另一个非空,说明不对称,返回 false。
- ❌ 当两个节点值不相等,说明不对称,返回 false。
以上三种情况涵盖了所有终止可能。注意,题目保证至少有一个节点,所以不需要考虑根节点为空的情况。
3. 确定单层递归逻辑
如果当前两个节点都不为空且值相等,那么我们需要继续比较它们的子树。根据对称的定义:
- 比较左节点的左子树 与 右节点的右子树
- 比较左节点的右子树 与 右节点的左子树
如果这两个递归调用都返回 true,则整体对称。这个逻辑可以用以下伪代码辅助理解:
function isSymmetric(left, right):
if left == null && right == null: return true
if left == null || right == null: return false
if left.val != right.val: return false
return isSymmetric(left.left, right.right) && isSymmetric(left.right, right.left)
这种设计巧妙地将复杂问题分解为对称子问题,是递归思想的经典应用。
代码实现(以 JavaScript 为例)
下面是递归解法的代码,注释清晰,可直接运行。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
bool isSymmetric(TreeNode* root) {
return check(root->left,root->right);
}
bool check(TreeNode* p,TreeNode* q){
// 两个都跨过叶子节点指向null
if (!p&&!q){
return true;
}
// 不是上面的情况,说明至少有一个非空,此时存在一个空/两个节点的值不相等说明不对称
if (!p||!q||p->val!=q->val){
return false;
}
//要求 L.left节点的值等于R.right节点的值 且 L.right节点的值等于R.left节点的值 才对称
return check(p->left,q->right)&&check(p->right,q->left);
}
};
以上代码的时间复杂度为 O(n),因为每个节点恰好被访问一次。空间复杂度为 O(n),取决于递归调用栈的深度,最坏情况下树退化为链表,递归深度为树的高度。
三、迭代解法:队列实现,另一种视角
除了递归,我们还可以用迭代法解决。核心思路是使用一个队列,每次取出两个节点进行比较,然后将它们的对称子节点成对入队,继续下一轮比较。
具体步骤如下:
- 将根节点的左孩子和右孩子加入队列。
- 每次从队列中取出两个节点,比较它们的值。
- 将左节点的左孩子与右节点的右孩子放入队列。
- 将左节点的右孩子与右节点的左孩子放入队列。
- 循环直到队列为空,或发现不对称返回 false。
这种方法避免了递归可能出现的栈溢出问题,在树的深度较大时更稳定。以下是 Java 语言的实现:
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root==null || (root.left==null && root.right==null)) {
return true;
}
//用队列保存节点
LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
//将根节点的左右孩子放到队列中
queue.add(root.left);
queue.add(root.right);
while(queue.size()>0) {
//从队列中取出两个节点,再比较这两个节点
TreeNode left = queue.removeFirst();
TreeNode right = queue.removeFirst();
//如果两个节点都为空就继续循环,两者有一个为空就返回false
if(left==null && right==null) {
continue;
}
if(left==null || right==null) {
return false;
}
if(left.val!=right.val) {
return false;
}
//将左节点的左孩子, 右节点的右孩子放入队列
queue.add(left.left);
queue.add(right.right);
//将左节点的右孩子,右节点的左孩子放入队列
queue.add(left.right);
queue.add(right.left);
}
return true;
}
}
迭代解法的时间复杂度同样为 O(n),空间复杂度为 O(n),因为队列中最多存储两层的节点。
在实际面试中,建议先给出递归解法,再补充迭代解法,展示你对问题多角度的理解。
四、常见错误与避坑指南
在实现过程中,不少初学者容易犯以下错误:
- 只比较左右孩子:这是最典型的错误。对称判断必须深入子树,而非仅仅比较根节点的左右孩子。
- 忽略空节点处理:在递归或迭代中,空指针的判断顺序至关重要,否则会导致空指针异常。
- 递归参数设计错误:有些同学会把递归函数设计为只接收一个节点,导致无法同时比较左右子树。
建议在写代码前,先在纸上画出树的结构,模拟递归调用的过程,能有效避免这些错误。
五、延伸思考与多语言实践
对称二叉树的思想可以迁移到其他问题,例如判断两棵树是否相同、求二叉树的最小深度等。此外,不同编程语言的实现也各有特色:
- C++:可以借助指针和引用,代码简洁高效。
- TypeScript:类型定义能增强代码可读性,适合大型项目。
- Go:结构体和方法的设计让树操作更直观。
如果你正在学习算法,建议尝试用至少两种语言实现本题,加深理解。下面简单展示 Go 语言的递归写法思路,其他语言可自行练习。
(此处可插入其他语言代码示例)
练习时,可以配合 LeetCode 的在线调试功能,逐步查看递归调用过程,会更有帮助。
总结一下,对称二叉树的核心在于理解对称的递归定义,并熟练掌握递归与迭代两种实现方式。建议先手写递归,再尝试用队列模拟迭代,最后对比两种方法的性能差异。希望这篇文章能帮你彻底掌握这道经典题目,举一反三,面试不慌!
如果你觉得本文有用,欢迎分享给更多学习算法的朋友,一起进步!
[AFFILIATE_SLOT_1] [AFFILIATE_SLOT_2]
浙公网安备 33010602011771号