文章目录
-
-
- 问题:101. 对称二叉树
- 解法1:递归法
-
- 递归三部曲
- 递归算法(两个后序遍历)
- 解法2:迭代法
- 解法3:使用栈
- 总结
-
问题:101. 对称二叉树
给定一个二叉树,检查它是否是镜像对称的。
核心:判断对称二叉树要比较的是哪两个节点,要比较的可不是左右节点!即对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了「其实我们要比较的是两个树(这两个树是根节点的左右子树)」,所以在递归遍历的过程中,也是要同时遍历两棵树。
那么如果比较呢?
比较的是两个子树的里侧和外侧的元素是否相等。那么遍历的顺序应该是什么样的呢?
本题遍历只能是“后序遍历”,因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等。
「正是因为要遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。」
但都可以理解算是后序遍历,尽管已经不是严格上在一个树上进行遍历的后序遍历了。
解法1:递归法
递归三部曲
第一,确定递归函数的参数和返回值
因为我们要比较的是根节点的两个子树是否是相互翻转的,进而判断这个树是不是对称树,所以要比较的是两个树,参数自然也是 根节点的左子树节点和右子树节点。
返回值自然是bool类型,为整个Solution的返回值,true表示相等,false表示不相等。
代码如下:
public boolean compare(TreeNode left,TreeNode right)
第二,确定终止条件
要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。
节点为空的情况有:(「注意我们比较的其实不是左孩子和右孩子,所以如下我称之为左节点右节点」)
- 根节点左节点为空,右节点不为空,不对称,return false
- 根节点左不为空,右为空,不对称 return false
- 根节点左右都为空,对称,返回true
此时已经排除掉了节点为空的情况,那么剩下的就是左右节点不为空:
左右都不为空,比较节点数值,不相同就return false
此时左右节点不为空,且数值也不相同的情况我们也处理了。
代码如下:
if (left==null && right==null) return true; if (left!=null && right==null) return false; if (left==null && right!=null) return false; if (left.val != right.val) return false;
注意上面最后一种情况,这里使用了if,而不是直接return false, 因为我们把以上情况都排除之后,剩下的就是 左右节点都不为空,且数值相同的情况,即第五种情况。
第三,确定单层递归的逻辑
此时才进入单层递归的逻辑,单层递归的逻辑就是处理 右节点都不为空,且数值相同的情况,即第五种情况。
- 比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
- 比较内测是否对称,传入左节点的右孩子,右节点的左孩子。
如果左右都对称就返回true ,有一侧不对称就返回false 。
代码如下:
boolean outSide = compare(left.left,right.right); // 因为是二叉树不是N叉树,所以只要比较左右节点,N叉树要比较1-N个孩子 boolean inSide = compare(left.right,right.left); boolean isSame = outSide && inSide; return isSame;
因为是二叉树不是N叉树,所以只要比较左右节点,N叉树要比较1-N个孩子
如上代码中,我们可以看出使用的遍历方式,左子树左右中,右子树右左中,所以我把这个遍历顺序也称之为“后序遍历”(尽管不是严格的后序遍历)。
递归算法(两个后序遍历)
最后递归的整体代码如下:
class Solution { public boolean isSymmetric(TreeNode root) { if (null == root) return true; // 处理空情况 return compare(root.left,root.right); } public boolean compare(TreeNode left,TreeNode right){ if (left==null && right==null) return true; if (left!=null && right==null) return false; if (left==null && right!=null) return false; if (left.val != right.val) return false; boolean outSide = compare(left.left,right.right); boolean inSide = compare(left.right,right.left); boolean isSame = outSide && inSide; return isSame; } }
「我给出的代码并不简洁,但是把每一步判断的逻辑都清楚的描绘出来了。」
如果上来就看网上各种简洁的代码,看起来真的很简单,但是很多逻辑都掩盖掉了,而题解可能也没有把掩盖掉的逻辑说清楚。
「盲目的照着抄,结果就是:发现这是一道“简单题”,稀里糊涂的就过了,但是真正的每一步判断逻辑未必想到清楚。」
解法2:迭代法
这道题目我们也可以使用迭代法,但要注意,这里的迭代法可不是前中后序的迭代写法,因为本题的本质是判断两个树是否是相互翻转的,其实已经不是所谓二叉树遍历的前中后序的关系了。
这里我们可以使用队列来比较两个树(根节点的左右子树)是否相互翻转,(「注意这不是层序遍历」)
使用队列
通过队列来判断根节点的左子树和右子树的内侧和外侧是否相等,如动画所示:
图片
如下的条件判断和递归的逻辑是一样的。
代码如下:
class Solution { public boolean isSymmetric(TreeNode root) { if (null == root) return true; Queue<TreeNode> queue = new LinkedList(); queue.offer(root.left); queue.offer(root.right); while (!queue.isEmpty()) { TreeNode left = queue.poll(); // 队列最头部元素 TreeNode right = queue.poll(); // 每次出两个 if (left == null && right == null) continue; // 这里不能返回true,递归这样可以,但是迭代不行,这样返回true,整个Solution就结束了 if (left != null && right == null) return false; // 三个false倒是可以直接返回,结束整个Solution if (left == null && right != null) return false; if (left.val != right.val) return false; // 顺序不能错,经过前面三个判断之后,这里的node.left和node.right不会为null,所以不会有空指针 queue.offer(left.left); queue.offer(right.right); // 添加外侧节点 queue.offer(left.right); queue.offer(right.left); // 添加内侧节点 } return true; // 能走出来就返回true 对称 } }
解法3:使用栈
细心的话,其实可以发现,这个迭代法,其实是把左右两个子树要比较的元素顺序放进一个容器,然后成对成对的取出来进行比较,那么其实使用栈也是可以的。
只要把队列原封不动的改成栈就可以了,我下面也给出了代码。
class Solution { public boolean isSymmetric(TreeNode root) { if (null == root) return true; Stack<TreeNode> stack = new Stack<>(); stack.push(root.left); stack.push(root.right); while (!stack.isEmpty()) { TreeNode left = stack.pop(); // 栈最头部元素 TreeNode right = stack.pop(); // 每次出两个 if (left == null && right == null) continue; // 这里不能返回true,递归这样可以,但是迭代不行,这样返回true,整个Solution就结束了 if (left != null && right == null) return false; if (left == null && right != null) return false; if (left.val != right.val) return false; // 顺序不能错,经过前面三个判断之后,这里的node.left和node.right不会为null,所以不会有空指针 stack.push(left.left); stack.push(right.right); // 添加外侧节点 stack.push(left.right); stack.push(right.left); // 添加内侧节点 } return true; // 能走出来就返回true 对称 } }
为什么队列和栈都可以?
因为while每次循环都包含了插入节点和取出节点,上一次循环中插入的节点就在下一次循环中取出,取出的时候是插入的倒序而已,比较的对象还是 外侧节点和外侧节点比较,内侧节点和内侧节点比较。
总结
这次我们又深度剖析了一道二叉树的“简单题”,大家会发现,真正的把题目搞清楚其实并不简单,leetcode上accept了和真正掌握了还是有距离的。
我们介绍了递归法和迭代法,递归依然通过递归三部曲来解决了这道题目,如果只看精简的代码根本看不出来递归三部曲是如果解题的。
在迭代法中我们使用了队列,需要注意的是这不是层序遍历,而且仅仅通过一个容器来成对的存放我们要比较的元素,知道这一本质之后就发现:用队列,用栈,甚至用数组,都是可以的。