目录
LeetCode226. 翻转二叉树
1. 思路
2. 代码实现
2.1 递归法实现(前中后序)
2.2 迭代法(前中后序)
2.3 统一迭代法
2.4 层序遍历实现
3. 复杂度分析
4. 思考
LeetCode101. 对称二叉树
方法一:递归解法
1. 思路
2. 代码实现
3. 复杂度分析
4. 思考
方法二:迭代解法
1. 思路
2. 代码实现
实现方式一:使用队列迭代
实现方式二:使用栈迭代
3. 复杂度分析
4. 思考
链接: 226. 翻转二叉树 - 力扣(LeetCode)
我们之前介绍的都是各种方式遍历二叉树,这次要翻转了,感觉还是有点懵逼。
这得怎么翻转呢?如果要从整个树来看,翻转还真的挺复杂,整个树以中间分割线进行翻转,如图:
可以发现想要翻转它,其实就把每一个节点的左右孩子交换一下就可以了。
关键在于遍历顺序,前中后序应该选哪一种遍历顺序? (一些同学这道题都过了,但是不知道自己用的是什么顺序)
遍历的过程中只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果。这道题目使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历(递归法)会把某些节点的左右孩子翻转了两次!建议拿纸画一画,就理解了。那么层序遍历可以不可以呢?依然可以的!只要把每一个节点的左右孩子翻转一下的遍历方式都是可以的!
中序遍历为啥不行?
以上面的二叉树为例,中序为左中右,先处理4的左孩子,把1和3交换顺序之后,再处理中节点,把2及其子树和7及其子树交换位置,现在该处理右节点了,但是发现右节点是之前处理过了的节点2及其子树,所以节点2及其子树会交换两遍,节点7及其子树没有处理。
用遍历二叉树的各种方法都可以实现,一共有10种。
包括递归法(前中后序)迭代法(前中后序)统一迭代法(前中后序)层序遍历
这里以前序递归为例实现:
# 前序递归实现 翻转二叉树 中左右
# time:O(N);space:O(N)
class Solution(object):
def invertTree(self, root):
"""
:type root: TreeNode
:rtype: TreeNode
"""
if root == None: return # base case
root.left, root.right = root.right,root.left #中
self.invertTree(root.left) # 左
self.invertTree(root.right) # 右
return root
# 翻转二叉树 递归法
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
# 递归 前序遍历
class Solution(object):
def invertTree(self, root):
"""
:type root: TreeNode
:rtype: TreeNode
"""
if root == None: return None
root.left,root.right = root.right,root.left
self.invertTree(root.left)
self.invertTree(root.right)
return root
# 递归 后序遍历
class Solution(object):
def invertTree(self, root):
"""
:type root: TreeNode
:rtype: TreeNode
"""
if root == None: return None
self.invertTree(root.left)
self.invertTree(root.right)
root.left,root.right = root.right,root.left
return root
# 递归 中序遍历
# time:O(N) space:O(logN)
class Solution(object):
def invertTree(self, root):
"""
:type root: TreeNode
:rtype: TreeNode
"""
if root == None: return None
self.invertTree(root.left)
root.left,root.right = root.right,root.left
# 这里还是要翻转左孩子,因为上面左右互换了
self.invertTree(root.left)
return root
# 迭代法 前序遍历
# time:O(N);space:O(N)
class Solution(object):
def invertTree(self, root):
"""
:type root: TreeNode
:rtype: TreeNode
"""
if root == None: return None
stack = [root]
while stack:
node = stack.pop()
node.left,node.right = node.right,node.left
if node.right: stack.append(node.right)
if node.left: stack.append(node.left)
return root
# 迭代法 中序遍历
# time O(N) space:O(N)
class Solution(object):
def invertTree(self, root):
"""
:type root: TreeNode
:rtype: TreeNode
"""
if root == None: return None
stack = []
cur = root
while cur or stack:
if cur:
stack.append(cur)
cur = cur.left
else:
cur = stack.pop()
cur.left,cur.right = cur.right,cur.left
# 前面左右顺序换了,所以这里还是处理左孩子
cur = cur.left
return root
# 迭代法 后序遍历
# time:O(N);space:O(N)
class Solution(object):
def invertTree(self, root):
"""
:type root: TreeNode
:rtype: TreeNode
"""
if root == None: return None
stack = [root]
while stack:
node = stack.pop()
node.left,node.right = node.right,node.left #中
if node.right: stack.append(node.right) #右
if node.left: stack.append(node.left) #左
return root
# 统一迭代法 前序
# time:o(n) space:O(N)
class Solution(object):
def invertTree(self, root):
"""
:type root: TreeNode
:rtype: TreeNode
"""
if root == None: return None
stack = [root]
while stack:
node = stack.pop()
if node != None:
if node.right: stack.append(node.right)
if node.left: stack.append(node.left)
stack.append(node)
stack.append(None)
else:
node = stack.pop()
node.left,node.right = node.right,node.left
return root
# 统一迭代法 中序
# time:o(n) space:O(N)
class Solution(object):
def invertTree(self, root):
"""
:type root: TreeNode
:rtype: TreeNode
"""
if root == None: return None
stack = [root]
while stack:
node = stack.pop()
if node != None:
if node.right: stack.append(node.right)
stack.append(node)
stack.append(None)
if node.left: stack.append(node.left)
else:
node = stack.pop()
node.left,node.right = node.right,node.left
return root
# 统一迭代法 后序
# time:o(n) space:O(N)
class Solution(object):
def invertTree(self, root):
"""
:type root: TreeNode
:rtype: TreeNode
"""
if root == None: return None
stack = [root]
while stack:
node = stack.pop()
if node != None:
stack.append(node)
stack.append(None)
if node.right: stack.append(node.right)
if node.left: stack.append(node.left)
else:
node = stack.pop()
node.left,node.right = node.right,node.left
return root
# 层序遍历 迭代
class Solution(object):
def invertTree(self, root):
"""
:type root: TreeNode
:rtype: TreeNode
"""
if root == None: return None
from collections import deque
queue = deque([root])
while queue:
size = len(queue)
for _ in range (size):
node = queue.popleft()
node.left,node.right = node.right,node.left
if node.left: queue.append(node.left)
if node.right: queue.append(node.right)
return root
本题的复杂度分析和二叉树的十种遍历方式的分析一模一样,可以参考。
Reference: 代码随想录 (programmercarl.com)
本题学习时间: 60分钟。
链接: 101. 对称二叉树 - 力扣(LeetCode)
首先想清楚,判断对称二叉树要比较的是哪两个节点,要比较的可不是左右节点!对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了其实我们要比较的是两个树(这两个树是根节点的左右子树),所以在递归遍历的过程中,也是要同时遍历两棵树。
那么如何比较呢?
比较的是两个子树的里侧和外侧的元素是否相等。如图所示:
那么遍历的顺序应该是什么样的呢?
本题遍历只能是“后序遍历”,因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等。
具体来说是因为要不断收集左右孩子的信息,返回给上一个节点,不断返回,才最后知道左子树和右子树是否可以相互翻转,后序遍历中,最后“中”的处理,就是将左右孩子的信息向上返回了;
如果是前序遍历,中左右,上来就在处理中间节点了,左右还没处理,我咋知道左边子树是否和右边子树相互翻转;同理中序遍历信息也不完善。
两个子树的遍历顺序一样吗?
**因为要遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。**但都可以理解算是后序遍历,尽管已经不是严格上在一个树上进行遍历的后序遍历了。
递归三部曲
因为我们要比较的是根节点的两个子树是否是相互翻转的,进而判断这个树是不是对称树,所以要比较的是两个树,参数自然也是左子树节点和右子树节点。
返回值自然是bool类型。
代码如下:
def compare(self, left, right):
要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。
节点为空的情况有:
(注意我们比较的其实不是左孩子和右孩子,所以如下我称之为左节点右节点)
此时已经排除掉了节点为空的情况,那么剩下的就是左右节点不为空:
此时左右节点不为空,且数值也不相同的情况我们也处理了。
代码如下:
if left == None and right != None: return False
elif left != None and right == None: return False
elif left == None and right == None: return True
#排除了空节点,再排除数值不相同的情况
elif left.val != right.val: return False
注意上面最后一种情况,我没有使用else,而是elseif, 因为我们把以上情况都排除之后,剩下的就是 左右节点都不为空,且数值相同的情况。
此时才进入单层递归的逻辑,单层递归的逻辑就是处理 左右节点都不为空,且数值相同的情况。
代码如下:
#此时就是:左右节点都不为空,且数值相同的情况
#此时才做递归,做下一层的判断
outside = self.compare(left.left, right.right) #左子树:左、 右子树:右
inside = self.compare(left.right, right.left) #左子树:右、 右子树:左
isSame = outside and inside #左子树:中、 右子树:中 (逻辑处理)
return isSame
如上代码中,我们可以看出使用的遍历方式,左子树左右中,右子树右左中,所以我把这个遍历顺序也称之为“后序遍历”(尽管不是严格的后序遍历)。
最后递归的Python整体代码如下:
# time:O(N);space:O(N)
class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
if not root:
return True
return self.compare(root.left, root.right)
def compare(self, left, right):
#首先排除空节点的情况
if left == None and right != None: return False
elif left != None and right == None: return False
elif left == None and right == None: return True
#排除了空节点,再排除数值不相同的情况
elif left.val != right.val: return False
#此时就是:左右节点都不为空,且数值相同的情况
#此时才做递归,做下一层的判断
#左子树:左、 右子树:右
outside = self.compare(left.left, right.right)
#左子树:右、 右子树:左
inside = self.compare(left.right, right.left)
#左子树:中、 右子树:中 (逻辑处理)
isSame = outside and inside
return isSame
给出的代码并不简洁,但是把每一步判断的逻辑都清楚的描绘出来了。
如果上来就看网上各种简洁的代码,看起来真的很简单,但是很多逻辑都掩盖掉了,而题解可能也没有把掩盖掉的逻辑说清楚。**盲目的照着抄,结果就是:发现这是一道“简单题”,稀里糊涂的就过了,但是真正的每一步判断逻辑未必想到清楚。**当然我可以把如上代码整理如下:
class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
if not root: return True
return self.compare(root.left, root.right)
def compare(self, left, right):
if left == None and right != None: return False
elif left != None and right == None: return False
elif left == None and right == None: return True
elif left.val != right.val: return False
else return self.compare(left.left, right.right) and
self.compare(left.right, right.left)
这个代码就很简洁了,但隐藏了很多逻辑,条理不清晰,而且递归三部曲,在这里完全体现不出来。所以建议大家做题的时候,一定要想清楚逻辑,每一步做什么。把道题目所有情况想到位,相应的代码写出来之后,再去追求简洁代码的效果。
假设树上一共 N 个节点。
时间复杂度:这里遍历了这棵树,时间复杂度为 O(n); 空间复杂度:这里的空间复杂度和递归使用的栈空间有关,这里递归层数不超过 n,故空间复杂度为 O(n)。
这道题目我们也可以使用迭代法,但要注意,这里的迭代法可不是前中后序的迭代写法,因为本题的本质是判断两个树是否是相互翻转的,其实已经不是所谓二叉树遍历的前中后序的关系了。
主要思路是把左右两个子树要比较的元素顺序放进一个容器,然后成对成对的取出来进行比较。
这里我们可以使用队列来比较两个树(根节点的左右子树)是否相互翻转,(注意这不是层序遍历)
# 迭代法 用队列
class Solution(object):
def isSymmetric(self, root):
"""
:type root: TreeNode
:rtype: bool
"""
if root == None: return True
from collections import deque
queue = deque()
queue.append(root.left) #将左子树头结点加入队列
queue.append(root.right) #将右子树头结点加入队列
while queue: #接下来就要判断这这两个树是否相互翻转
leftNode = queue.popleft()
rightNode = queue.popleft()
#左节点为空、右节点为空,此时说明是对称的
if leftNode == None and rightNode == None: continue
# 因为这是迭代的做法,所以不能直接return True 应该要继续比较
elif leftNode != None and rightNode == None: return False
elif leftNode == None and rightNode != None: return False
elif leftNode.val != rightNode.val: return False
else:
queue.append(leftNode.left) #加入左节点左孩子
queue.append(rightNode.right) #加入右节点右孩子
queue.append(leftNode.right) #加入左节点右孩子
queue.append(rightNode.left) #加入右节点左孩子
return True
其实使用栈也是可以的。只要把队列原封不动的改成栈就可以了
# 迭代法 使用栈
# time:o(N);space:o(N)
class Solution(object):
def isSymmetric(self, root):
"""
:type root: TreeNode
:rtype: bool
"""
if root == None: return True
stack = []
stack.append(root.right)
stack.append(root.left)
while stack:
leftNode = stack.pop()
rightNode = stack.pop()
if not leftNode and not rightNode: continue
# 上面那三行的判断可以简略为这一行判断
elif not leftNode or not rightNode or
leftNode.val != rightNode.val:
return False
else:
stack.append(leftNode.left)
stack.append(rightNode.right)
stack.append(leftNode.right)
stack.append(rightNode.left)
return True
时间复杂度:O(N)
N为二叉树的节点数目,这里遍历了这颗二叉树,因此时间复杂度为O(N);
空间复杂度:O(N)
这里需要用一个队列/栈来维护节点,每个节点最多进队一次,出队一次,队列中最多不会超过 n 个点,故空间复杂度为 O(n)。
Reference: 代码随想录 (programmercarl.com)
本题学习时间: 60分钟。
本篇的学习大约花了2小时,总字数9500+,在学习完二叉树的10种遍历方式之后,本篇的翻转二叉树是对10种遍历方式的应用;对称二叉树是同时遍历两个二叉树的应用。(求推荐!)